From 5c3315251b57c93ce578fd3161fb0fae39ea5ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 25 Aug 2018 20:58:50 +0200 Subject: [PATCH 01/27] first step to buffer redesign: class TerminalLine; discourage low level index access to cells --- src/Buffer.test.ts | 55 ++-- src/Buffer.ts | 7 +- src/InputHandler.ts | 35 +-- src/Linkifier.test.ts | 7 +- src/Linkifier.ts | 3 +- src/SelectionManager.test.ts | 18 +- src/SelectionManager.ts | 39 +-- src/Terminal.integration.ts | 2 +- src/Terminal.test.ts | 240 +++++++++--------- src/Terminal.ts | 13 +- src/TerminalLine.ts | 58 +++++ src/Types.ts | 7 +- src/handlers/AltClickHandler.ts | 3 +- src/renderer/CharacterJoinerRegistry.test.ts | 52 ++-- src/renderer/CharacterJoinerRegistry.ts | 11 +- src/renderer/CursorRenderLayer.ts | 2 +- src/renderer/TextRenderLayer.ts | 4 +- .../dom/DomRendererRowFactory.test.ts | 31 +-- src/renderer/dom/DomRendererRowFactory.ts | 5 +- src/utils/TestUtils.test.ts | 11 +- 20 files changed, 357 insertions(+), 246 deletions(-) create mode 100644 src/TerminalLine.ts diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index a51e5456..82d5bd2b 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -8,6 +8,7 @@ import { ITerminal } from './Types'; import { Buffer } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; +import { TerminalLine } from './TerminalLine'; 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 = terminal.blankLine().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); } } }); @@ -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'); }); }); }); @@ -272,34 +273,43 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { - buffer.lines.set(0, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, 'a', 1, 'a'.charCodeAt(0)], [ null, 'b', 1, 'b'.charCodeAt(0)], [ null, 'c', 1, 'c'.charCodeAt(0)], [ null, 'd', 1, 'd'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, '語', 2, 35486 ], [ null, '', 0, null], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, '語', 2, '語'.charCodeAt(0) ], [ null, '', 0, null], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); const str0 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str0, '語'); @@ -312,10 +322,13 @@ describe('Buffer', () => { }); it('should handle single width emojis', () => { - buffer.lines.set(0, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, '😁', 1, '😁'.charCodeAt(0) ], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '😁'); @@ -325,10 +338,13 @@ describe('Buffer', () => { }); it('should handle double width emojis', () => { - buffer.lines.set(0, [ + const line = new TerminalLine(); + let data: [number, string, number, number][] = [ [ null, '😁', 2, '😁'.charCodeAt(0) ], [ null, '', 0, null] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '😁'); @@ -336,11 +352,14 @@ describe('Buffer', () => { const str2 = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str2, '😁'); - buffer.lines.set(0, [ + const line2 = new TerminalLine(); + data = [ [ null, '😁', 2, '😁'.charCodeAt(0) ], [ null, '', 0, null], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line2.push(data[i]); + 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..42ab01b5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -7,6 +7,7 @@ import { CircularList } from './common/CircularList'; import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; +import { TerminalLine } from './TerminalLine'; 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; @@ -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(); @@ -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 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 42b1b095..08486cd0 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -381,18 +381,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 +414,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 +437,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 +450,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); @@ -929,7 +932,7 @@ export class InputHandler extends Disposable implements IInputHandler { 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; + buffer.lines.get(row).set(j++, ch); } } @@ -988,10 +991,10 @@ export class InputHandler extends Disposable implements IInputHandler { 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]; + const ch = line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; while (param--) { - line[buffer.x++] = ch; + line.set(buffer.x++, ch); } } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 7f4d5603..183571e8 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -9,6 +9,7 @@ import { ILinkMatcher, LineData, ITerminal } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './common/CircularList'; +import { TerminalLine } from './TerminalLine'; 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): TerminalLine { + const result = new TerminalLine(); 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 9ab770d9..c82bf513 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -7,6 +7,7 @@ import { IMouseZoneManager } from './ui/Types'; import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, LineData } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; +import { TerminalLine } from './TerminalLine'; /** * The Linkifier applies links to rows shortly after they have been refreshed. @@ -169,7 +170,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { return; } // If the first row is wrapped, backtrack to find the origin row and linkify that - let line: LineData; + let line: TerminalLine; do { rowIndex--; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 20e1ca60..70e26fb4 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,6 +10,7 @@ import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; +import { TerminalLine } from './TerminalLine'; 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): TerminalLine { + const result = new TerminalLine(); 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[]): TerminalLine { + const line = new TerminalLine(); + 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 TerminalLine(); + 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, '中文'); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 7f7204fc..422dbc14 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -11,6 +11,7 @@ import { EventEmitter } from './EventEmitter'; import { SelectionModel } from './SelectionModel'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { AltClickHandler } from './handlers/AltClickHandler'; +import { TerminalLine } from './TerminalLine'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -204,7 +205,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 +216,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 +501,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 +591,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 +662,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: TerminalLine, 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 +734,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 +765,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 +809,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 +824,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..47aeb44b 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+$/, ''); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 0ea854e2..4344553e 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -337,62 +337,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 +403,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 +654,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 +667,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 +679,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 +693,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 +704,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 +717,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 +749,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 +777,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 +788,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 +798,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 +809,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 +823,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 +834,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 +847,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 +858,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 +868,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 +879,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 +892,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 +903,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 +918,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 +930,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 +943,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 a310d543..5e3f8f50 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -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 { TerminalLine } from './TerminalLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -1719,7 +1720,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm for (; x < this.cols; x++) { - line[x] = ch; + line.set(x, ch); } this.updateRange(y); } @@ -1737,7 +1738,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm x++; while (x--) { - line[x] = ch; + line.set(x, ch); } this.updateRange(y); } @@ -1777,21 +1778,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @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 { + public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { 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 = []; + const line = new TerminalLine(); // 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; + line.isWrapped = isWrapped; } cols = cols || this.cols; for (let i = 0; i < cols; i++) { - line[i] = ch; + line.set(i, ch); } return line; diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts new file mode 100644 index 00000000..fcaa99df --- /dev/null +++ b/src/TerminalLine.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { CharData } from './Types'; + +export class TerminalLine { + private _data: CharData[]; + public isWrapped = false; + length: number; + constructor() { + this._data = []; + this.length = this._data.length; + + // for debugging purpose: + // throw Error when something tries to do number index access + // TODO: remove when done with transition + for (let i = 0; i < 100; ++i) { + Object.defineProperty(this, i, { + get: () => { + throw new Error('get per index access is disabled'); + }, + set: (value: any) => { + throw new Error('set per index access is disabled'); + } + }); + } + + } + get(index: number): CharData { + return this._data[index]; + } + set(index: number, data: CharData): void { + this._data[index] = data; + // TODO: unref old, ref new + } + pop(): CharData | undefined { + // TODO: unref here, change CharData to [typeof Attributes, ...] + const data = this._data.pop(); + this.length = this._data.length; + return data; + } + push(data: CharData): void { + this._data.push(data); + this.length = this._data.length; + // TODO: ref here + } + splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { + const removed = this._data.splice(start, deleteCount, ...items); + this.length = this._data.length; + // TODO: ref new, unref old + return removed; + } + /** to be called when a line gets removed */ + release(): void { + // TODO: unref here + } +} diff --git a/src/Types.ts b/src/Types.ts index ba1a7990..f7784b5f 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -7,6 +7,7 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, import { IColorSet, IRenderer } from './renderer/Types'; import { IMouseZoneManager } from './ui/Types'; import { ICharset } from './core/Types'; +import { TerminalLine } from './TerminalLine'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -74,7 +75,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { eraseRight(x: number, y: number): void; eraseLine(y: number): void; eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): LineData; + blankLine(cur?: boolean, isWrapped?: boolean): TerminalLine; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -233,7 +234,7 @@ 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; + blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine; } export interface IBufferAccessor { @@ -272,7 +273,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { } export interface IBuffer { - readonly lines: ICircularList; + readonly lines: ICircularList; ydisp: number; ybase: number; y: number; diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 7ac084db..48556ac5 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -5,6 +5,7 @@ import { ITerminal, ICircularList, LineData } from '../Types'; import { C0 } from '../common/data/EscapeSequences'; +import { TerminalLine } from '../TerminalLine'; const enum Direction { UP = 'A', @@ -18,7 +19,7 @@ export class AltClickHandler { private _startCol: number; private _endRow: number; private _endCol: number; - private _lines: ICircularList; + private _lines: ICircularList; constructor( private _mouseEvent: MouseEvent, diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 85d94601..d427ef5e 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -1,11 +1,11 @@ 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 { TerminalLine } from '../TerminalLine'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; @@ -14,22 +14,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 TerminalLine()); + 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 +260,19 @@ describe('CharacterJoinerRegistry', () => { }); }); -function lineData(line: string, attr: number = 0): LineData { - return line.split('').map(char => [attr, char, 1, char.charCodeAt(0)]); +interface IPartialLineData { + [0]: string; + [1]?: number; +} + +function lineData(data: IPartialLineData[]): TerminalLine { + const tline = new TerminalLine(); + 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..b8d1a3cd 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,6 +1,7 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { ITerminal, LineData } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; +import { TerminalLine } from '../TerminalLine'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { @@ -51,10 +52,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 +116,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: TerminalLine, 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 +141,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: TerminalLine, startCol: number): void { let currentRangeIndex = 0; let currentRangeStarted = false; let currentStringIndex = 0; @@ -152,7 +153,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..32284615 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -9,11 +9,12 @@ 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 { TerminalLine } from '../../TerminalLine'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; let rowFactory: DomRendererRowFactory; - let lineData: LineData; + let lineData: TerminalLine; 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): TerminalLine { + const lineData = new TerminalLine(); 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..d2a43b06 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -6,6 +6,7 @@ import { LineData } from '../../Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; import { FLAGS } from '../Types'; +import { TerminalLine } from '../../TerminalLine'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; @@ -17,7 +18,7 @@ export class DomRendererRowFactory { ) { } - public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: TerminalLine, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); let colCount = 0; @@ -27,7 +28,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..81754397 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -8,6 +8,7 @@ import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITermi import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; +import { TerminalLine } from '../TerminalLine'; export class MockTerminal implements ITerminal { markers: IMarker[]; @@ -145,8 +146,8 @@ 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 = []; + blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { + const line = new TerminalLine(); cols = cols || this.cols; for (let i = 0; i < cols; i++) { line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); @@ -228,7 +229,7 @@ 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][] { + blankLine(cur?: boolean, isWrapped?: boolean): TerminalLine { throw new Error('Method not implemented.'); } prevStop(x?: number): number { @@ -295,7 +296,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 +319,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; } } From 0340d91150b7e30770088e541989543725711e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 25 Aug 2018 20:58:50 +0200 Subject: [PATCH 02/27] first step to buffer redesign: class TerminalLine; discourage low level index access to cells --- src/Buffer.test.ts | 55 ++-- src/Buffer.ts | 7 +- src/InputHandler.ts | 35 +-- src/Linkifier.test.ts | 7 +- src/Linkifier.ts | 5 +- src/SelectionManager.test.ts | 18 +- src/SelectionManager.ts | 39 +-- src/Terminal.integration.ts | 2 +- src/Terminal.test.ts | 240 +++++++++--------- src/Terminal.ts | 13 +- src/TerminalLine.ts | 58 +++++ src/Types.ts | 7 +- src/handlers/AltClickHandler.ts | 3 +- src/renderer/CharacterJoinerRegistry.test.ts | 52 ++-- src/renderer/CharacterJoinerRegistry.ts | 11 +- src/renderer/CursorRenderLayer.ts | 2 +- src/renderer/TextRenderLayer.ts | 4 +- .../dom/DomRendererRowFactory.test.ts | 31 +-- src/renderer/dom/DomRendererRowFactory.ts | 5 +- src/utils/TestUtils.test.ts | 11 +- 20 files changed, 358 insertions(+), 247 deletions(-) create mode 100644 src/TerminalLine.ts diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index a51e5456..82d5bd2b 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -8,6 +8,7 @@ import { ITerminal } from './Types'; import { Buffer } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; +import { TerminalLine } from './TerminalLine'; 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 = terminal.blankLine().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); } } }); @@ -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'); }); }); }); @@ -272,34 +273,43 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { - buffer.lines.set(0, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, 'a', 1, 'a'.charCodeAt(0)], [ null, 'b', 1, 'b'.charCodeAt(0)], [ null, 'c', 1, 'c'.charCodeAt(0)], [ null, 'd', 1, 'd'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, '語', 2, 35486 ], [ null, '', 0, null], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, '語', 2, '語'.charCodeAt(0) ], [ null, '', 0, null], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); const str0 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str0, '語'); @@ -312,10 +322,13 @@ describe('Buffer', () => { }); it('should handle single width emojis', () => { - buffer.lines.set(0, [ + const line = new TerminalLine(); + const data: [number, string, number, number][] = [ [ null, '😁', 1, '😁'.charCodeAt(0) ], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '😁'); @@ -325,10 +338,13 @@ describe('Buffer', () => { }); it('should handle double width emojis', () => { - buffer.lines.set(0, [ + const line = new TerminalLine(); + let data: [number, string, number, number][] = [ [ null, '😁', 2, '😁'.charCodeAt(0) ], [ null, '', 0, null] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '😁'); @@ -336,11 +352,14 @@ describe('Buffer', () => { const str2 = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str2, '😁'); - buffer.lines.set(0, [ + const line2 = new TerminalLine(); + data = [ [ null, '😁', 2, '😁'.charCodeAt(0) ], [ null, '', 0, null], [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line2.push(data[i]); + 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..42ab01b5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -7,6 +7,7 @@ import { CircularList } from './common/CircularList'; import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; +import { TerminalLine } from './TerminalLine'; 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; @@ -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(); @@ -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 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 42b1b095..08486cd0 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -381,18 +381,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 +414,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 +437,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 +450,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); @@ -929,7 +932,7 @@ export class InputHandler extends Disposable implements IInputHandler { 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; + buffer.lines.get(row).set(j++, ch); } } @@ -988,10 +991,10 @@ export class InputHandler extends Disposable implements IInputHandler { 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]; + const ch = line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; while (param--) { - line[buffer.x++] = ch; + line.set(buffer.x++, ch); } } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 7f4d5603..183571e8 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -9,6 +9,7 @@ import { ILinkMatcher, LineData, ITerminal } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './common/CircularList'; +import { TerminalLine } from './TerminalLine'; 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): TerminalLine { + const result = new TerminalLine(); 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..8c8a20c5 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -7,6 +7,7 @@ import { IMouseZoneManager } from './ui/Types'; import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, LineData } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; +import { TerminalLine } from './TerminalLine'; import { CHAR_DATA_ATTR_INDEX } from './Buffer'; /** @@ -170,7 +171,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { return; } // If the first row is wrapped, backtrack to find the origin row and linkify that - let line: LineData; + let line: TerminalLine; do { rowIndex--; @@ -219,7 +220,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..70e26fb4 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,6 +10,7 @@ import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; +import { TerminalLine } from './TerminalLine'; 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): TerminalLine { + const result = new TerminalLine(); 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[]): TerminalLine { + const line = new TerminalLine(); + 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 TerminalLine(); + 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, '中文'); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 7f7204fc..422dbc14 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -11,6 +11,7 @@ import { EventEmitter } from './EventEmitter'; import { SelectionModel } from './SelectionModel'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { AltClickHandler } from './handlers/AltClickHandler'; +import { TerminalLine } from './TerminalLine'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -204,7 +205,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 +216,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 +501,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 +591,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 +662,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: TerminalLine, 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 +734,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 +765,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 +809,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 +824,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..47aeb44b 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+$/, ''); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 0ea854e2..4344553e 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -337,62 +337,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 +403,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 +654,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 +667,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 +679,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 +693,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 +704,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 +717,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 +749,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 +777,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 +788,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 +798,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 +809,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 +823,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 +834,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 +847,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 +858,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 +868,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 +879,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 +892,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 +903,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 +918,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 +930,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 +943,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 a310d543..5e3f8f50 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -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 { TerminalLine } from './TerminalLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -1719,7 +1720,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm for (; x < this.cols; x++) { - line[x] = ch; + line.set(x, ch); } this.updateRange(y); } @@ -1737,7 +1738,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm x++; while (x--) { - line[x] = ch; + line.set(x, ch); } this.updateRange(y); } @@ -1777,21 +1778,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @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 { + public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { 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 = []; + const line = new TerminalLine(); // 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; + line.isWrapped = isWrapped; } cols = cols || this.cols; for (let i = 0; i < cols; i++) { - line[i] = ch; + line.set(i, ch); } return line; diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts new file mode 100644 index 00000000..fcaa99df --- /dev/null +++ b/src/TerminalLine.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { CharData } from './Types'; + +export class TerminalLine { + private _data: CharData[]; + public isWrapped = false; + length: number; + constructor() { + this._data = []; + this.length = this._data.length; + + // for debugging purpose: + // throw Error when something tries to do number index access + // TODO: remove when done with transition + for (let i = 0; i < 100; ++i) { + Object.defineProperty(this, i, { + get: () => { + throw new Error('get per index access is disabled'); + }, + set: (value: any) => { + throw new Error('set per index access is disabled'); + } + }); + } + + } + get(index: number): CharData { + return this._data[index]; + } + set(index: number, data: CharData): void { + this._data[index] = data; + // TODO: unref old, ref new + } + pop(): CharData | undefined { + // TODO: unref here, change CharData to [typeof Attributes, ...] + const data = this._data.pop(); + this.length = this._data.length; + return data; + } + push(data: CharData): void { + this._data.push(data); + this.length = this._data.length; + // TODO: ref here + } + splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { + const removed = this._data.splice(start, deleteCount, ...items); + this.length = this._data.length; + // TODO: ref new, unref old + return removed; + } + /** to be called when a line gets removed */ + release(): void { + // TODO: unref here + } +} diff --git a/src/Types.ts b/src/Types.ts index 4917c513..92fe9ed2 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -7,6 +7,7 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, import { IColorSet, IRenderer } from './renderer/Types'; import { IMouseZoneManager } from './ui/Types'; import { ICharset } from './core/Types'; +import { TerminalLine } from './TerminalLine'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -74,7 +75,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { eraseRight(x: number, y: number): void; eraseLine(y: number): void; eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): LineData; + blankLine(cur?: boolean, isWrapped?: boolean): TerminalLine; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -234,7 +235,7 @@ 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; + blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine; } export interface IBufferAccessor { @@ -273,7 +274,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { } export interface IBuffer { - readonly lines: ICircularList; + readonly lines: ICircularList; ydisp: number; ybase: number; y: number; diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 7ac084db..48556ac5 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -5,6 +5,7 @@ import { ITerminal, ICircularList, LineData } from '../Types'; import { C0 } from '../common/data/EscapeSequences'; +import { TerminalLine } from '../TerminalLine'; const enum Direction { UP = 'A', @@ -18,7 +19,7 @@ export class AltClickHandler { private _startCol: number; private _endRow: number; private _endCol: number; - private _lines: ICircularList; + private _lines: ICircularList; constructor( private _mouseEvent: MouseEvent, diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 85d94601..d427ef5e 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -1,11 +1,11 @@ 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 { TerminalLine } from '../TerminalLine'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; @@ -14,22 +14,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 TerminalLine()); + 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 +260,19 @@ describe('CharacterJoinerRegistry', () => { }); }); -function lineData(line: string, attr: number = 0): LineData { - return line.split('').map(char => [attr, char, 1, char.charCodeAt(0)]); +interface IPartialLineData { + [0]: string; + [1]?: number; +} + +function lineData(data: IPartialLineData[]): TerminalLine { + const tline = new TerminalLine(); + 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..b8d1a3cd 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,6 +1,7 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { ITerminal, LineData } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; +import { TerminalLine } from '../TerminalLine'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { @@ -51,10 +52,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 +116,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: TerminalLine, 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 +141,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: TerminalLine, startCol: number): void { let currentRangeIndex = 0; let currentRangeStarted = false; let currentStringIndex = 0; @@ -152,7 +153,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..32284615 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -9,11 +9,12 @@ 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 { TerminalLine } from '../../TerminalLine'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; let rowFactory: DomRendererRowFactory; - let lineData: LineData; + let lineData: TerminalLine; 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): TerminalLine { + const lineData = new TerminalLine(); 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..d2a43b06 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -6,6 +6,7 @@ import { LineData } from '../../Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; import { FLAGS } from '../Types'; +import { TerminalLine } from '../../TerminalLine'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; @@ -17,7 +18,7 @@ export class DomRendererRowFactory { ) { } - public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: TerminalLine, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); let colCount = 0; @@ -27,7 +28,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..81754397 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -8,6 +8,7 @@ import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITermi import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; +import { TerminalLine } from '../TerminalLine'; export class MockTerminal implements ITerminal { markers: IMarker[]; @@ -145,8 +146,8 @@ 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 = []; + blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { + const line = new TerminalLine(); cols = cols || this.cols; for (let i = 0; i < cols; i++) { line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); @@ -228,7 +229,7 @@ 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][] { + blankLine(cur?: boolean, isWrapped?: boolean): TerminalLine { throw new Error('Method not implemented.'); } prevStop(x?: number): number { @@ -295,7 +296,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 +319,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; } } From a99cac45f7b10851027078cbf91ec2681bcd41ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 25 Aug 2018 22:56:15 +0200 Subject: [PATCH 03/27] fix import errors --- src/Buffer.ts | 2 +- src/Linkifier.test.ts | 2 +- src/Linkifier.ts | 2 +- src/SelectionManager.test.ts | 2 +- src/Terminal.ts | 2 +- src/TerminalLine.ts | 2 +- src/handlers/AltClickHandler.ts | 2 +- src/renderer/CharacterJoinerRegistry.ts | 2 +- src/renderer/dom/DomRendererRowFactory.test.ts | 1 - src/renderer/dom/DomRendererRowFactory.ts | 1 - src/utils/TestUtils.test.ts | 2 +- 11 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 42ab01b5..d98f1425 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,7 +4,7 @@ */ import { CircularList } from './common/CircularList'; -import { LineData, CharData, ITerminal, IBuffer } from './Types'; +import { CharData, ITerminal, IBuffer } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; import { TerminalLine } from './TerminalLine'; diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 183571e8..0a066284 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './ui/Types'; -import { ILinkMatcher, LineData, ITerminal } from './Types'; +import { ILinkMatcher, ITerminal } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './common/CircularList'; diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 8c8a20c5..a9d8423e 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 } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; import { TerminalLine } from './TerminalLine'; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 70e26fb4..bd9c8cbb 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -8,7 +8,7 @@ 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 } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; import { TerminalLine } from './TerminalLine'; diff --git a/src/Terminal.ts b/src/Terminal.ts index 5e3f8f50..5a6528c1 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'; diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index fcaa99df..6e87a212 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -16,7 +16,7 @@ export class TerminalLine { // throw Error when something tries to do number index access // TODO: remove when done with transition for (let i = 0; i < 100; ++i) { - Object.defineProperty(this, i, { + Object.defineProperty(this, i.toString(), { get: () => { throw new Error('get per index access is disabled'); }, diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 48556ac5..fcafcddb 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 } from '../Types'; import { C0 } from '../common/data/EscapeSequences'; import { TerminalLine } from '../TerminalLine'; diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index b8d1a3cd..2b50f5ac 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 } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; import { TerminalLine } from '../TerminalLine'; diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 32284615..9503a46c 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,7 +6,6 @@ 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 { TerminalLine } from '../../TerminalLine'; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index d2a43b06..1c259549 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,7 +3,6 @@ * @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 { TerminalLine } from '../../TerminalLine'; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 81754397..0d14d471 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -4,7 +4,7 @@ */ 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 { 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 * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; From cd8477a942e1fac85de167eab7e8c09c85f55255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 26 Aug 2018 14:27:38 +0200 Subject: [PATCH 04/27] move blankLine to TerminalLine --- src/Buffer.test.ts | 8 ++++---- src/Buffer.ts | 4 ++-- src/InputHandler.ts | 9 +++++---- src/Terminal.test.ts | 9 +++++---- src/Terminal.ts | 25 +++++-------------------- src/TerminalLine.ts | 20 ++++++++++++++++++++ src/utils/TestUtils.test.ts | 9 ++------- 7 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 82d5bd2b..ca1437d9 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,7 +5,7 @@ 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 { TerminalLine } from './TerminalLine'; @@ -37,7 +37,7 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = terminal.blankLine().get(0); + const blankLineChar = TerminalLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0); buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { @@ -180,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(TerminalLine.blankLine(terminal.cols, DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -200,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(TerminalLine.blankLine(terminal.cols, DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; diff --git a/src/Buffer.ts b/src/Buffer.ts index d98f1425..c34a3786 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -85,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(TerminalLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } } } @@ -147,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(TerminalLine.blankLine(newCols, DEFAULT_ATTR)); } } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 08486cd0..064d9f1c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -13,6 +13,7 @@ import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { Disposable } from './common/Lifecycle'; +import { TerminalLine } from './TerminalLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -815,7 +816,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, TerminalLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); } // this.maxRange(); @@ -845,7 +846,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, TerminalLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); } // this.maxRange(); @@ -887,7 +888,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, TerminalLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -906,7 +907,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, TerminalLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 4344553e..322e6d6c 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 { TerminalLine } from './TerminalLine'; 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), TerminalLine.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), TerminalLine.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), TerminalLine.blankLine(term.cols, DEFAULT_ATTR)); } }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 5a6528c1..28dced6c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1170,7 +1170,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 = TerminalLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1757,7 +1757,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffer.ybase = 0; this.buffer.y = 0; for (let i = 1; i < this.rows; i++) { - this.buffer.lines.push(this.blankLine()); + this.buffer.lines.push(TerminalLine.blankLine(this.cols, DEFAULT_ATTR)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); @@ -1778,24 +1778,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param cols The number of columns in the terminal, if this is not * set, the terminal's current column count would be used. */ + // FIXME: can this be removed after transition to TerminalLine.blankLine? public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { - 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 = new TerminalLine(); - - // 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.set(i, ch); - } - - return line; + return TerminalLine.blankLine(cols || this.cols, cur ? this.eraseAttr() : DEFAULT_ATTR, isWrapped); } /** @@ -1884,7 +1869,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, TerminalLine.blankLine(this.cols, this.eraseAttr())); this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); } else { diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index 6e87a212..cf4f467c 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -3,8 +3,26 @@ * @license MIT */ import { CharData } from './Types'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +/** + * Class representing a terminal line. + * Currently the class is a thin proxy to `CharData[]`. + * Once the storages are in place it will proxy access to + * typed array based line data. + * TODO: move typical line actions in `InputHandler` and `Terminal` here: + * - create blank line + * - insert cells + * - remove cells + */ export class TerminalLine { + static blankLine(cols: number, attr: number, isWrapped?: boolean): TerminalLine { + const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + const line = new TerminalLine(); + if (isWrapped) line.isWrapped = true; + for (let i = 0; i < cols; i++) line.push(ch); + return line; + } private _data: CharData[]; public isWrapped = false; length: number; @@ -15,6 +33,7 @@ export class TerminalLine { // for debugging purpose: // throw Error when something tries to do number index access // TODO: remove when done with transition + /* for (let i = 0; i < 100; ++i) { Object.defineProperty(this, i.toString(), { get: () => { @@ -25,6 +44,7 @@ export class TerminalLine { } }); } + */ } get(index: number): CharData { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 0d14d471..2e60be9e 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -5,7 +5,7 @@ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types'; -import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer'; +import { Buffer } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; import { TerminalLine } from '../TerminalLine'; @@ -147,12 +147,7 @@ export class MockTerminal implements ITerminal { throw new Error('Method not implemented.'); } blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { - const line = new TerminalLine(); - 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; + return TerminalLine.blankLine(this.cols, 0); } registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } deregisterCharacterJoiner(joinerId: number): void { } From a5534152162ee9b7dcff08972dd0944c1071beb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 26 Aug 2018 14:35:28 +0200 Subject: [PATCH 05/27] remove blankLine from Terminal --- src/Terminal.ts | 12 ------------ src/Types.ts | 2 -- src/utils/TestUtils.test.ts | 6 ------ 3 files changed, 20 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 28dced6c..457de0fc 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1771,18 +1771,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II 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. - */ - // FIXME: can this be removed after transition to TerminalLine.blankLine? - public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): TerminalLine { - return TerminalLine.blankLine(cols || this.cols, cur ? this.eraseAttr() : DEFAULT_ATTR, isWrapped); - } - /** * If cur return the back color xterm feature attribute. Else return default attribute. * @param cur diff --git a/src/Types.ts b/src/Types.ts index 92fe9ed2..ecec9a95 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -75,7 +75,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { eraseRight(x: number, y: number): void; eraseLine(y: number): void; eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): TerminalLine; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -235,7 +234,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): TerminalLine; } export interface IBufferAccessor { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 2e60be9e..a279b48b 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -146,9 +146,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): TerminalLine { - return TerminalLine.blankLine(this.cols, 0); - } registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } deregisterCharacterJoiner(joinerId: number): void { } } @@ -224,9 +221,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { eraseLeft(x: number, y: number): void { throw new Error('Method not implemented.'); } - blankLine(cur?: boolean, isWrapped?: boolean): TerminalLine { - throw new Error('Method not implemented.'); - } prevStop(x?: number): number { throw new Error('Method not implemented.'); } From cee432a66a4e26242b4bc8aad0b9bd9e5724abae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 26 Aug 2018 15:48:53 +0200 Subject: [PATCH 06/27] TerminalLine ctor parameters; tests --- src/TerminalLine.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++ src/TerminalLine.ts | 23 ++++++++++------- 2 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 src/TerminalLine.test.ts diff --git a/src/TerminalLine.test.ts b/src/TerminalLine.test.ts new file mode 100644 index 00000000..e5a57aa5 --- /dev/null +++ b/src/TerminalLine.test.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ +import * as chai from 'chai'; +import { TerminalLine } from './TerminalLine'; +import { CharData } 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'; + +describe('TerminalLine', function(): void { + it('ctor', function(): void { + let line = new TerminalLine(); + chai.expect(line.length).equals(0); + chai.expect(line.pop()).equals(undefined); + chai.expect(line.isWrapped).equals(false); + line = new TerminalLine(10); + chai.expect(line.length).equals(10); + chai.expect(line.pop()).eql(TerminalLine.defaultCell); + chai.expect(line.isWrapped).equals(false); + line = new TerminalLine(10, null, true); + chai.expect(line.length).equals(10); + chai.expect(line.pop()).eql(TerminalLine.defaultCell); + chai.expect(line.isWrapped).equals(true); + line = new TerminalLine(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 TerminalLine(); + 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 = TerminalLine.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); + }); +}); diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index cf4f467c..fa13197f 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ import { CharData } from './Types'; @@ -11,25 +11,28 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; * Once the storages are in place it will proxy access to * typed array based line data. * TODO: move typical line actions in `InputHandler` and `Terminal` here: - * - create blank line + * - create blank line - done * - insert cells * - remove cells + * - maybe Buffer.translateBufferLineToString */ export class TerminalLine { + static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; static blankLine(cols: number, attr: number, isWrapped?: boolean): TerminalLine { const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - const line = new TerminalLine(); - if (isWrapped) line.isWrapped = true; - for (let i = 0; i < cols; i++) line.push(ch); - return line; + return new TerminalLine(cols, ch, isWrapped); } private _data: CharData[]; public isWrapped = false; length: number; - constructor() { + constructor(cols?: number, ch?: CharData, isWrapped?: boolean) { this._data = []; this.length = this._data.length; - + if (cols) { + if (!ch) ch = TerminalLine.defaultCell; + for (let i = 0; i < cols; i++) this.push(ch); // Note: the ctor ch is not cloned + } + if (isWrapped) this.isWrapped = true; // for debugging purpose: // throw Error when something tries to do number index access // TODO: remove when done with transition @@ -45,7 +48,6 @@ export class TerminalLine { }); } */ - } get(index: number): CharData { return this._data[index]; @@ -75,4 +77,7 @@ export class TerminalLine { release(): void { // TODO: unref here } + toArray(): CharData[] { + return this._data; + } } From 77cb6979907e49aa9296fb346b4d9e6057702cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 26 Aug 2018 16:04:52 +0200 Subject: [PATCH 07/27] remove casts for .isWrapped --- src/Buffer.test.ts | 20 ++++++++++---------- src/Buffer.ts | 4 ++-- src/Linkifier.ts | 6 +++--- src/SelectionManager.test.ts | 12 ++++++------ src/SelectionManager.ts | 4 ++-- src/addons/winptyCompat/winptyCompat.ts | 2 +- src/handlers/AltClickHandler.ts | 6 +++--- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index ca1437d9..087e2f64 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -67,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 }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index c34a3786..b71537dc 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -269,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/Linkifier.ts b/src/Linkifier.ts index a9d8423e..859df661 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -165,7 +165,7 @@ 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; @@ -182,14 +182,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); } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index bd9c8cbb..c7374ad5 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -192,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(); @@ -206,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(); @@ -345,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 422dbc14..fcd45f2a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -811,7 +811,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager if (followWrappedLinesAbove) { 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.get(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; @@ -826,7 +826,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager if (followWrappedLinesBelow) { 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.get(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/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 25ad7d91..c6b33b27 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -34,7 +34,7 @@ export function winptyCompatInit(terminal: Terminal): void { 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 fcafcddb..2932dd5c 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -139,7 +139,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++; } } @@ -153,12 +153,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; From 9c68eb3b1136e32abdb668fb86fa3d8adff01a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 26 Aug 2018 19:24:49 +0200 Subject: [PATCH 08/27] move line content functions to TerminalLine --- src/InputHandler.ts | 121 ++++++++++++++++-------------------- src/Terminal.integration.ts | 1 + src/Terminal.ts | 19 ++---- src/TerminalLine.ts | 25 +++++++- 4 files changed, 82 insertions(+), 84 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 064d9f1c..6c72e6a4 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'; @@ -550,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); } /** @@ -723,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). @@ -739,22 +746,24 @@ 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; - for (; j < this._terminal.rows; j++) { - this._terminal.eraseLine(j); - } + 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._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; - while (j--) { - this._terminal.eraseLine(j); - } + this._terminal.updateRange(j); + this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1); + while (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) @@ -784,15 +793,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); } /** @@ -859,22 +869,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); } /** @@ -920,21 +920,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).set(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] + ); } /** @@ -986,17 +976,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.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - - while (param--) { - line.set(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/Terminal.integration.ts b/src/Terminal.integration.ts index 47aeb44b..66fd3502 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -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.ts b/src/Terminal.ts index 457de0fc..c5537cc4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1715,13 +1715,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ 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.set(x, ch); - } + if (!line) return; + line.replaceCells(x, this.cols, [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); this.updateRange(y); } @@ -1732,14 +1727,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ 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.set(x, ch); - } + if (!line) return; + line.replaceCells(0, x + 1, [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); this.updateRange(y); } diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index fa13197f..f88ca1f4 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -12,9 +12,12 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; * typed array based line data. * TODO: move typical line actions in `InputHandler` and `Terminal` here: * - create blank line - done - * - insert cells - * - remove cells + * - insert cells - done + * - remove cells - done * - maybe Buffer.translateBufferLineToString + * + * next steps towards typed array: + * - replace all external push/pop/splice accesses */ export class TerminalLine { static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; @@ -80,4 +83,22 @@ export class TerminalLine { toArray(): CharData[] { return this._data; } + /** insert n cells ch at pos, right cells are lost (stable length) */ + 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) */ + 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 */ + replaceCells(start: number, end: number, fill: CharData): void { + while (start < end && start < this.length) this.set(start++, fill); // Note: fill is not cloned + } } From d4c621a182e8af4687a3b272d640765e7db5ae26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 26 Aug 2018 19:36:21 +0200 Subject: [PATCH 09/27] more TerminalLine tests --- src/Terminal.ts | 3 +++ src/TerminalLine.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/TerminalLine.ts | 35 ++++++++++++----------------------- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c5537cc4..e5e3c755 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1713,6 +1713,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param x The column from which to start erasing to the end of the line. * @param y The line in which to operate. */ + // FIXME: decide whether to remove from Terminal public eraseRight(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); if (!line) return; @@ -1725,6 +1726,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param x The column from which to start erasing to the start of the line. * @param y The line in which to operate. */ + // FIXME: decide whether to remove from Terminal public eraseLeft(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); if (!line) return; @@ -1756,6 +1758,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Erase all content in the given line * @param y The line to erase all of its contents. */ + // FIXME: decide whether to remove from Terminal public eraseLine(y: number): void { this.eraseRight(0, y); } diff --git a/src/TerminalLine.test.ts b/src/TerminalLine.test.ts index e5a57aa5..72cea26d 100644 --- a/src/TerminalLine.test.ts +++ b/src/TerminalLine.test.ts @@ -50,4 +50,41 @@ describe('TerminalLine', function(): void { 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 TerminalLine(); + 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 TerminalLine(); + 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 TerminalLine(); + 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/TerminalLine.ts b/src/TerminalLine.ts index f88ca1f4..1fd184d6 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -10,14 +10,15 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; * Currently the class is a thin proxy to `CharData[]`. * Once the storages are in place it will proxy access to * typed array based line data. - * TODO: move typical line actions in `InputHandler` and `Terminal` here: - * - create blank line - done - * - insert cells - done - * - remove cells - done - * - maybe Buffer.translateBufferLineToString - * - * next steps towards typed array: - * - replace all external push/pop/splice accesses + * TODO: + * - move Buffer.translateBufferLineToString here? + * - next steps towards typed array: + * - create ITerminalLine interface w'o length methods + * - resize method + * - replace all external push/pop/splice accesses + * - fixed length + * - remove push/pop/splice + * - implement typed array alternative once string is removed from CharData */ export class TerminalLine { static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; @@ -36,21 +37,6 @@ export class TerminalLine { for (let i = 0; i < cols; i++) this.push(ch); // Note: the ctor ch is not cloned } if (isWrapped) this.isWrapped = true; - // for debugging purpose: - // throw Error when something tries to do number index access - // TODO: remove when done with transition - /* - for (let i = 0; i < 100; ++i) { - Object.defineProperty(this, i.toString(), { - get: () => { - throw new Error('get per index access is disabled'); - }, - set: (value: any) => { - throw new Error('set per index access is disabled'); - } - }); - } - */ } get(index: number): CharData { return this._data[index]; @@ -59,17 +45,20 @@ export class TerminalLine { this._data[index] = data; // TODO: unref old, ref new } + // to be removed for typed array pop(): CharData | undefined { // TODO: unref here, change CharData to [typeof Attributes, ...] const data = this._data.pop(); this.length = this._data.length; return data; } + // to be removed for typed array push(data: CharData): void { this._data.push(data); this.length = this._data.length; // TODO: ref here } + // to be removed for typed array splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { const removed = this._data.splice(start, deleteCount, ...items); this.length = this._data.length; From 6576d513b0ee2291070fe61007af3968b850cbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 27 Aug 2018 16:53:58 +0200 Subject: [PATCH 10/27] fix index access to cell in winptyCompat --- src/addons/winptyCompat/winptyCompat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index c6b33b27..2513dce6 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -30,7 +30,7 @@ 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); From f964a34b341b7830e57716645a306ac6de6e0841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 27 Aug 2018 17:01:30 +0200 Subject: [PATCH 11/27] explicit member visibility --- src/TerminalLine.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index 1fd184d6..7f944191 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -28,7 +28,8 @@ export class TerminalLine { } private _data: CharData[]; public isWrapped = false; - length: number; + public length: number; + constructor(cols?: number, ch?: CharData, isWrapped?: boolean) { this._data = []; this.length = this._data.length; @@ -38,56 +39,65 @@ export class TerminalLine { } if (isWrapped) this.isWrapped = true; } - get(index: number): CharData { + + public get(index: number): CharData { return this._data[index]; } - set(index: number, data: CharData): void { + + public set(index: number, data: CharData): void { this._data[index] = data; // TODO: unref old, ref new } + // to be removed for typed array - pop(): CharData | undefined { + public pop(): CharData | undefined { // TODO: unref here, change CharData to [typeof Attributes, ...] const data = this._data.pop(); this.length = this._data.length; return data; } + // to be removed for typed array - push(data: CharData): void { + public push(data: CharData): void { this._data.push(data); this.length = this._data.length; // TODO: ref here } + // to be removed for typed array - splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { + public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { const removed = this._data.splice(start, deleteCount, ...items); this.length = this._data.length; // TODO: ref new, unref old return removed; } + /** to be called when a line gets removed */ - release(): void { + public release(): void { // TODO: unref here } - toArray(): CharData[] { + public toArray(): CharData[] { return this._data; } + /** insert n cells ch at pos, right cells are lost (stable length) */ - insertCells(pos: number, n: number, ch: CharData): void { + 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) */ - deleteCells(pos: number, n: number, fill: CharData): void { + 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 */ - replaceCells(start: number, end: number, fill: CharData): void { + public replaceCells(start: number, end: number, fill: CharData): void { while (start < end && start < this.length) this.set(start++, fill); // Note: fill is not cloned } } From de26be56c13fad355a19aac041df9c437fc7ece6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 27 Aug 2018 17:11:40 +0200 Subject: [PATCH 12/27] change type of IPartialLineData --- src/TerminalLine.ts | 2 +- src/renderer/CharacterJoinerRegistry.test.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index 7f944191..186e2f58 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -95,7 +95,7 @@ export class TerminalLine { 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 diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index d427ef5e..d7cd1ce4 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -260,16 +260,13 @@ describe('CharacterJoinerRegistry', () => { }); }); -interface IPartialLineData { - [0]: string; - [1]?: number; -} +type IPartialLineData = ([string] | [string, number]); function lineData(data: IPartialLineData[]): TerminalLine { const tline = new TerminalLine(); for (let i = 0; i < data.length; ++i) { const line = data[i][0]; - const attr = data[i][1] || 0; + const attr = (data[i][1] || 0); line.split('').map(char => tline.push([attr, char, 1, char.charCodeAt(0)])); } return tline; From 424d3257d11c834b283d5412dc606157a438dab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 28 Aug 2018 02:34:46 +0200 Subject: [PATCH 13/27] set conditional blocks in brackets --- src/InputHandler.ts | 12 +++++++++--- src/Terminal.ts | 8 ++++++-- src/TerminalLine.ts | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 6c72e6a4..b6883a0f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -749,20 +749,26 @@ export class InputHandler extends Disposable implements IInputHandler { 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._eraseInBufferLine(j, 0, this._terminal.cols); + for (; j < this._terminal.rows; j++) { + this._eraseInBufferLine(j, 0, this._terminal.cols); + } this._terminal.updateRange(j); break; case 1: j = this._terminal.buffer.y; this._terminal.updateRange(j); this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1); - while (j--) this._eraseInBufferLine(j, 0, this._terminal.cols); + while (j--) { + this._eraseInBufferLine(j, 0, this._terminal.cols); + } this._terminal.updateRange(0); break; case 2: j = this._terminal.rows; this._terminal.updateRange(j - 1); - while (j--) this._eraseInBufferLine(j, 0, this._terminal.cols); + while (j--) { + this._eraseInBufferLine(j, 0, this._terminal.cols); + } this._terminal.updateRange(0); break; case 3: diff --git a/src/Terminal.ts b/src/Terminal.ts index e5e3c755..9f800b37 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1716,7 +1716,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // FIXME: decide whether to remove from Terminal public eraseRight(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) return; + if (!line) { + return; + } line.replaceCells(x, this.cols, [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); this.updateRange(y); } @@ -1729,7 +1731,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // FIXME: decide whether to remove from Terminal public eraseLeft(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) return; + if (!line) { + return; + } line.replaceCells(0, x + 1, [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); this.updateRange(y); } diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index 186e2f58..77f3962e 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -34,10 +34,16 @@ export class TerminalLine { this._data = []; this.length = this._data.length; if (cols) { - if (!ch) ch = TerminalLine.defaultCell; - for (let i = 0; i < cols; i++) this.push(ch); // Note: the ctor ch is not cloned + if (!ch) { + ch = TerminalLine.defaultCell; + } + for (let i = 0; i < cols; i++) { + this.push(ch); // Note: the ctor ch is not cloned + } + } + if (isWrapped) { + this.isWrapped = true; } - if (isWrapped) this.isWrapped = true; } public get(index: number): CharData { @@ -98,6 +104,8 @@ export class TerminalLine { /** 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 + while (start < end && start < this.length) { + this.set(start++, fill); // Note: fill is not cloned + } } } From 52f132a5706b1e222bf6eec54471dfad202ea4b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 28 Aug 2018 14:29:18 -0700 Subject: [PATCH 14/27] Clean up buffer tests --- src/Buffer.test.ts | 52 +++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 087e2f64..e2db9636 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -274,13 +274,10 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { const line = new TerminalLine(); - const data: [number, string, number, number][] = [ - [ null, 'a', 1, 'a'.charCodeAt(0)], - [ null, 'b', 1, 'b'.charCodeAt(0)], - [ null, 'c', 1, 'c'.charCodeAt(0)], - [ null, 'd', 1, 'd'.charCodeAt(0)] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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); @@ -289,12 +286,9 @@ describe('Buffer', () => { it('should handle a cut-off double width character by including it', () => { const line = new TerminalLine(); - const data: [number, string, number, number][] = [ - [ null, '語', 2, 35486 ], - [ null, '', 0, null], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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); @@ -303,12 +297,9 @@ describe('Buffer', () => { it('should handle a zero width character in the middle of the string by not including it', () => { const line = new TerminalLine(); - const data: [number, string, number, number][] = [ - [ null, '語', 2, '語'.charCodeAt(0) ], - [ null, '', 0, null], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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); @@ -323,11 +314,8 @@ describe('Buffer', () => { it('should handle single width emojis', () => { const line = new TerminalLine(); - const data: [number, string, number, number][] = [ - [ null, '😁', 1, '😁'.charCodeAt(0) ], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); + 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); @@ -339,11 +327,8 @@ describe('Buffer', () => { it('should handle double width emojis', () => { const line = new TerminalLine(); - let data: [number, string, number, number][] = [ - [ null, '😁', 2, '😁'.charCodeAt(0) ], - [ null, '', 0, null] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); + line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); + line.push([ null, '', 0, null]); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -353,12 +338,9 @@ describe('Buffer', () => { assert.equal(str2, '😁'); const line2 = new TerminalLine(); - data = [ - [ null, '😁', 2, '😁'.charCodeAt(0) ], - [ null, '', 0, null], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]; - for (let i = 0; i < data.length; ++i) line2.push(data[i]); + 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); From 6439f6e85614d75815ce02f6d85fa4c2a06765d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 29 Aug 2018 22:37:01 +0200 Subject: [PATCH 15/27] regression tests for changes in InputHandler --- src/InputHandler.test.ts | 322 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 321 insertions(+), 1 deletion(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index e8099914..b82c0e38 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -3,9 +3,12 @@ * @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 { TerminalLine } from './TerminalLine'; +import { Terminal } from './Terminal'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -84,4 +87,321 @@ describe('InputHandler', () => { assert.equal(terminal.bracketedPasteMode, false); }); }); + describe('regression tests', function(): void { + type CharData = [number, string, number, number]; + + function lineContent(line: TerminalLine): 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() { + 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: TerminalLine = term.buffer.lines.get(0); // line for old variant + const line2: TerminalLine = 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: TerminalLine = term.buffer.lines.get(0); // line for old variant + const line2: TerminalLine = 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); + + function eraseInLine(params: number[]): void { + switch (params[0]) { + case 0: + term.eraseRight(term.buffer.x, term.buffer.y); + break; + case 1: + term.eraseLeft(term.buffer.x, term.buffer.y); + break; + case 2: + term.eraseLine(term.buffer.y); + break; + } + } + + // 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; + 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; + 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; + 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 InputHandler(termOld); + const termNew = new Terminal(); + const inputHandlerNew = new InputHandler(termNew); + + function eraseInDisplay(params: number[]): void { + let j; + switch (params[0]) { + case 0: + termOld.eraseRight(termOld.buffer.x, termOld.buffer.y); + j = termOld.buffer.y + 1; + for (; j < termOld.rows; j++) { + termOld.eraseLine(j); + } + break; + case 1: + termOld.eraseLeft(termOld.buffer.x, termOld.buffer.y); + j = termOld.buffer.y; + while (j--) { + termOld.eraseLine(j); + } + break; + case 2: + j = termOld.rows; + while (j--) termOld.eraseLine(j); + break; + case 3: + // Clear scrollback (everything not in viewport) + const scrollBackSize = termOld.buffer.lines.length - termOld.rows; + if (scrollBackSize > 0) { + termOld.buffer.lines.trimStart(scrollBackSize); + termOld.buffer.ybase = Math.max(termOld.buffer.ybase - scrollBackSize, 0); + termOld.buffer.ydisp = Math.max(termOld.buffer.ydisp - scrollBackSize, 0); + // Force a scroll event to refresh viewport + termOld.emit('scroll', 0); + } + break; + } + } + + // 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; + 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; + 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; + eraseInDisplay([2]); + termNew.buffer.y = 5; + termNew.buffer.x = 40; + inputHandlerNew.eraseInDisplay([2]); + expect(termContent(termNew)).eql(termContent(termOld)); + }); + }); }); From 01a4ec21da1f773f3b4ac23ac2568b830500ed00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 29 Aug 2018 22:41:24 +0200 Subject: [PATCH 16/27] make linter happy --- src/InputHandler.test.ts | 98 ++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index b82c0e38..22a80c18 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -102,7 +102,7 @@ describe('InputHandler', () => { return result; } - it('insertChars', function() { + it('insertChars', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); @@ -110,10 +110,10 @@ describe('InputHandler', () => { 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 @@ -124,57 +124,57 @@ describe('InputHandler', () => { } // insert some data in first and second line - inputHandler.parse(Array(term.cols - 9).join("a")); + inputHandler.parse(Array(term.cols - 9).join('a')); inputHandler.parse('1234567890'); - inputHandler.parse(Array(term.cols - 9).join("a")); + inputHandler.parse(Array(term.cols - 9).join('a')); inputHandler.parse('1234567890'); const line1: TerminalLine = term.buffer.lines.get(0); // line for old variant const line2: TerminalLine = 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'); - + 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'); + 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(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'); + 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(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'); + 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(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") + ' '); + 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(Array(term.cols - 9).join('a') + ' '); expect(lineContent(line2)).equals(lineContent(line1)); }); it('deleteChars', function(): void { @@ -187,10 +187,10 @@ describe('InputHandler', () => { 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--) { @@ -201,57 +201,57 @@ describe('InputHandler', () => { } // insert some data in first and second line - inputHandler.parse(Array(term.cols - 9).join("a")); + inputHandler.parse(Array(term.cols - 9).join('a')); inputHandler.parse('1234567890'); - inputHandler.parse(Array(term.cols - 9).join("a")); + inputHandler.parse(Array(term.cols - 9).join('a')); inputHandler.parse('1234567890'); const line1: TerminalLine = term.buffer.lines.get(0); // line for old variant const line2: TerminalLine = 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'); + 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 '); + 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(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 '); + 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(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 '); + 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(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") + ' '); + 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(Array(term.cols - 9).join('a') + ' '); expect(lineContent(line2)).equals(lineContent(line1)); }); it('eraseInLine', function(): void { @@ -273,42 +273,42 @@ describe('InputHandler', () => { } // 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")); + 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; eraseInLine([0]); - expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join("a") + ' '); + 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") + ' '); + expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' '); // params[1] - left erase term.buffer.y = 2; term.buffer.x = 70; eraseInLine([1]); - expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(" ") + ' aaaaaaaaa'); + 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'); + expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa'); // params[1] - left erase term.buffer.y = 4; term.buffer.x = 70; eraseInLine([2]); - expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(" ")); + 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(" ")); + expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' ')); }); it('eraseInDisplay', function(): void { @@ -353,10 +353,10 @@ describe('InputHandler', () => { } // 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")); + 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")); + 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)); @@ -374,8 +374,8 @@ describe('InputHandler', () => { 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")); + 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; @@ -391,8 +391,8 @@ describe('InputHandler', () => { 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")); + 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; From 381e6c174057128597d3a14ca435354bf8624807 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 30 Aug 2018 07:57:03 -0700 Subject: [PATCH 17/27] Move defaultCell to an exported member --- src/TerminalLine.test.ts | 6 +++--- src/TerminalLine.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/TerminalLine.test.ts b/src/TerminalLine.test.ts index 72cea26d..f0fd1498 100644 --- a/src/TerminalLine.test.ts +++ b/src/TerminalLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { TerminalLine } from './TerminalLine'; +import { TerminalLine, defaultCell } from './TerminalLine'; import { CharData } 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'; @@ -15,11 +15,11 @@ describe('TerminalLine', function(): void { chai.expect(line.isWrapped).equals(false); line = new TerminalLine(10); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(TerminalLine.defaultCell); + chai.expect(line.pop()).eql(defaultCell); chai.expect(line.isWrapped).equals(false); line = new TerminalLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(TerminalLine.defaultCell); + chai.expect(line.pop()).eql(defaultCell); chai.expect(line.isWrapped).equals(true); line = new TerminalLine(10, [123, 'a', 456, 789], true); chai.expect(line.length).equals(10); diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index 77f3962e..a053bab8 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -5,6 +5,8 @@ import { CharData } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +export const defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + /** * Class representing a terminal line. * Currently the class is a thin proxy to `CharData[]`. @@ -21,11 +23,11 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; * - implement typed array alternative once string is removed from CharData */ export class TerminalLine { - static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - static blankLine(cols: number, attr: number, isWrapped?: boolean): TerminalLine { + public static blankLine(cols: number, attr: number, isWrapped?: boolean): TerminalLine { const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; return new TerminalLine(cols, ch, isWrapped); } + private _data: CharData[]; public isWrapped = false; public length: number; @@ -35,7 +37,7 @@ export class TerminalLine { this.length = this._data.length; if (cols) { if (!ch) { - ch = TerminalLine.defaultCell; + ch = defaultCell; } for (let i = 0; i < cols; i++) { this.push(ch); // Note: the ctor ch is not cloned @@ -82,6 +84,7 @@ export class TerminalLine { public release(): void { // TODO: unref here } + public toArray(): CharData[] { return this._data; } From 59fc4af9a0f1fa7c69cacf2b60f25e57fe0dc557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 17:52:45 +0200 Subject: [PATCH 18/27] IBufferLine interface --- src/Buffer.test.ts | 20 +++++------ src/Buffer.ts | 12 +++---- src/InputHandler.test.ts | 12 +++---- src/InputHandler.ts | 10 +++--- src/Linkifier.test.ts | 10 +++--- src/Linkifier.ts | 5 ++- src/SelectionManager.test.ts | 14 ++++---- src/SelectionManager.ts | 5 ++- src/Terminal.test.ts | 8 ++--- src/Terminal.ts | 8 ++--- src/TerminalLine.test.ts | 35 +++++++++++-------- src/TerminalLine.ts | 23 ++++-------- src/Types.ts | 19 ++++++++-- src/handlers/AltClickHandler.ts | 5 ++- src/renderer/CharacterJoinerRegistry.test.ts | 11 +++--- src/renderer/CharacterJoinerRegistry.ts | 7 ++-- .../dom/DomRendererRowFactory.test.ts | 9 ++--- src/renderer/dom/DomRendererRowFactory.ts | 4 +-- src/utils/TestUtils.test.ts | 7 ++-- 19 files changed, 116 insertions(+), 108 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index e2db9636..ded814de 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -8,7 +8,7 @@ import { ITerminal } from './Types'; import { Buffer, DEFAULT_ATTR } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; -import { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -37,7 +37,7 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = TerminalLine.blankLine(terminal.cols, DEFAULT_ATTR).get(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++) { @@ -180,7 +180,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(TerminalLine.blankLine(terminal.cols, DEFAULT_ATTR)); + buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -200,7 +200,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(TerminalLine.blankLine(terminal.cols, DEFAULT_ATTR)); + buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -273,7 +273,7 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { - const line = new TerminalLine(); + 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)]); @@ -285,7 +285,7 @@ describe('Buffer', () => { }); it('should handle a cut-off double width character by including it', () => { - const line = new TerminalLine(); + const line = new BufferLine(); line.push([ null, '語', 2, 35486 ]); line.push([ null, '', 0, null]); line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); @@ -296,7 +296,7 @@ describe('Buffer', () => { }); it('should handle a zero width character in the middle of the string by not including it', () => { - const line = new TerminalLine(); + const line = new BufferLine(); line.push([ null, '語', 2, '語'.charCodeAt(0) ]); line.push([ null, '', 0, null]); line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); @@ -313,7 +313,7 @@ describe('Buffer', () => { }); it('should handle single width emojis', () => { - const line = new TerminalLine(); + const line = new BufferLine(); line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]); line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); buffer.lines.set(0, line); @@ -326,7 +326,7 @@ describe('Buffer', () => { }); it('should handle double width emojis', () => { - const line = new TerminalLine(); + const line = new BufferLine(); line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); line.push([ null, '', 0, null]); buffer.lines.set(0, line); @@ -337,7 +337,7 @@ describe('Buffer', () => { const str2 = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str2, '😁'); - const line2 = new TerminalLine(); + const line2 = new BufferLine(); line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); line2.push([ null, '', 0, null]); line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]); diff --git a/src/Buffer.ts b/src/Buffer.ts index b71537dc..40cbf29e 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,10 +4,10 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; -import { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -28,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; @@ -85,7 +85,7 @@ export class Buffer implements IBuffer { if (this.lines.length === 0) { let i = this._terminal.rows; while (i--) { - this.lines.push(TerminalLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); + this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } } } @@ -98,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(); @@ -147,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(TerminalLine.blankLine(newCols, DEFAULT_ATTR)); + this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR)); } } } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 22a80c18..4774c105 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -7,8 +7,8 @@ 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 { TerminalLine } from './TerminalLine'; import { Terminal } from './Terminal'; +import { IBufferLine } from './Types'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -90,7 +90,7 @@ describe('InputHandler', () => { describe('regression tests', function(): void { type CharData = [number, string, number, number]; - function lineContent(line: TerminalLine): string { + 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; @@ -128,8 +128,8 @@ describe('InputHandler', () => { inputHandler.parse('1234567890'); inputHandler.parse(Array(term.cols - 9).join('a')); inputHandler.parse('1234567890'); - const line1: TerminalLine = term.buffer.lines.get(0); // line for old variant - const line2: TerminalLine = term.buffer.lines.get(1); // line for new variant + 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'); @@ -205,8 +205,8 @@ describe('InputHandler', () => { inputHandler.parse('1234567890'); inputHandler.parse(Array(term.cols - 9).join('a')); inputHandler.parse('1234567890'); - const line1: TerminalLine = term.buffer.lines.get(0); // line for old variant - const line2: TerminalLine = term.buffer.lines.get(1); // line for new variant + 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'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index b6883a0f..ca743575 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -13,7 +13,7 @@ import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { Disposable } from './common/Lifecycle'; -import { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -832,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, TerminalLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); + buffer.lines.splice(row, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); } // this.maxRange(); @@ -862,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, TerminalLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); + buffer.lines.splice(j, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); } // this.maxRange(); @@ -894,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, TerminalLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -913,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.scrollBottom, 0, TerminalLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0a066284..0f84d41c 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,11 +5,11 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './ui/Types'; -import { ILinkMatcher, 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 { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { @@ -43,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): TerminalLine { - const result = new TerminalLine(); + 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 859df661..8615daf8 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,10 +4,9 @@ */ import { IMouseZoneManager } from './ui/Types'; -import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferLine } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; -import { TerminalLine } from './TerminalLine'; import { CHAR_DATA_ATTR_INDEX } from './Buffer'; /** @@ -171,7 +170,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { return; } // If the first row is wrapped, backtrack to find the origin row and linkify that - let line: TerminalLine; + let line: IBufferLine; do { rowIndex--; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c7374ad5..880a42d3 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -8,9 +8,9 @@ import { CharMeasure } from './ui/CharMeasure'; import { SelectionManager, SelectionMode } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; -import { ITerminal, IBuffer } from './Types'; +import { ITerminal, IBuffer, IBufferLine } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; -import { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -53,16 +53,16 @@ describe('SelectionManager', () => { selectionManager = new TestSelectionManager(terminal, null); }); - function stringToRow(text: string): TerminalLine { - const result = new TerminalLine(); + 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[]): TerminalLine { - const line = new TerminalLine(); + function stringArrayToRow(chars: string[]): IBufferLine { + const line = new BufferLine(); chars.map(c => line.push([0, c, 1, c.charCodeAt(0)])); return line; } @@ -100,7 +100,7 @@ describe('SelectionManager', () => { }); it('should expand selection for wide characters', () => { // Wide characters use a special format - const line = new TerminalLine(); + const line = new BufferLine(); const data: [number, string, number, number][] = [ [null, '中', 2, '中'.charCodeAt(0)], [null, '', 0, null], diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index fcd45f2a..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'; @@ -11,7 +11,6 @@ import { EventEmitter } from './EventEmitter'; import { SelectionModel } from './SelectionModel'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { AltClickHandler } from './handlers/AltClickHandler'; -import { TerminalLine } from './TerminalLine'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -662,7 +661,7 @@ 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: TerminalLine, 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.get(i); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 322e6d6c..4f765151 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -7,7 +7,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; -import { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -142,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), TerminalLine.blankLine(term.cols, DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); } }); it('should clear a buffer larger than rows', () => { @@ -159,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), TerminalLine.blankLine(term.cols, DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); } }); it('should not break the prompt when cleared twice', () => { @@ -172,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), TerminalLine.blankLine(term.cols, DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); } }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index fadff43d..56d98afd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,7 +52,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; -import { TerminalLine } from './TerminalLine'; +import { BufferLine } from './TerminalLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -1171,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 = TerminalLine.blankLine(this.cols, DEFAULT_ATTR, 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; @@ -1753,7 +1753,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffer.ybase = 0; this.buffer.y = 0; for (let i = 1; i < this.rows; i++) { - this.buffer.lines.push(TerminalLine.blankLine(this.cols, DEFAULT_ATTR)); + this.buffer.lines.push(BufferLine.blankLine(this.cols, DEFAULT_ATTR)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); @@ -1854,7 +1854,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, TerminalLine.blankLine(this.cols, this.eraseAttr())); + 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/TerminalLine.test.ts b/src/TerminalLine.test.ts index 72cea26d..19a4d4c7 100644 --- a/src/TerminalLine.test.ts +++ b/src/TerminalLine.test.ts @@ -3,31 +3,38 @@ * @license MIT */ import * as chai from 'chai'; -import { TerminalLine } from './TerminalLine'; -import { CharData } from './Types'; +import { BufferLine } from './TerminalLine'; +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'; -describe('TerminalLine', function(): void { + +class TestBufferLine extends BufferLine { + public toArray(): CharData[] { + return this._data; + } +} + +describe('BufferLine', function(): void { it('ctor', function(): void { - let line = new TerminalLine(); + 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 TerminalLine(10); + line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(TerminalLine.defaultCell); + chai.expect(line.pop()).eql(TestBufferLine.defaultCell); chai.expect(line.isWrapped).equals(false); - line = new TerminalLine(10, null, true); + line = new TestBufferLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(TerminalLine.defaultCell); + chai.expect(line.pop()).eql(TestBufferLine.defaultCell); chai.expect(line.isWrapped).equals(true); - line = new TerminalLine(10, [123, 'a', 456, 789], 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 TerminalLine(); + const line = new TestBufferLine(); const data: CharData[] = [ [1, 'a', 0, 0], [2, 'b', 0, 0], @@ -41,7 +48,7 @@ describe('TerminalLine', function(): void { chai.expect(line.toArray()).eql(data); }); it('TerminalLine.blankLine', function(): void { - const line = TerminalLine.blankLine(5, 123); + const line = TestBufferLine.blankLine(5, 123); chai.expect(line.length).equals(5); chai.expect(line.isWrapped).equals(false); const ch = line.get(0); @@ -51,7 +58,7 @@ describe('TerminalLine', function(): void { chai.expect(ch[CHAR_DATA_CODE_INDEX]).equals(NULL_CELL_CODE); }); it('insertCells', function(): void { - const line = new TerminalLine(); + const line = new TestBufferLine(); const data: CharData[] = [ [1, 'a', 0, 0], [2, 'b', 0, 0], @@ -62,7 +69,7 @@ describe('TerminalLine', function(): void { chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [4, 'd', 0, 0]]); }); it('deleteCells', function(): void { - const line = new TerminalLine(); + const line = new TestBufferLine(); const data: CharData[] = [ [1, 'a', 0, 0], [2, 'b', 0, 0], @@ -75,7 +82,7 @@ describe('TerminalLine', function(): void { 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 TerminalLine(); + const line = new TestBufferLine(); const data: CharData[] = [ [1, 'a', 0, 0], [2, 'b', 0, 0], diff --git a/src/TerminalLine.ts b/src/TerminalLine.ts index 77f3962e..51e0148b 100644 --- a/src/TerminalLine.ts +++ b/src/TerminalLine.ts @@ -2,7 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData } from './Types'; +import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; /** @@ -20,13 +20,13 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; * - remove push/pop/splice * - implement typed array alternative once string is removed from CharData */ -export class TerminalLine { +export class BufferLine implements IBufferLine { static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - static blankLine(cols: number, attr: number, isWrapped?: boolean): TerminalLine { + static blankLine(cols: number, attr: number, isWrapped?: boolean): BufferLine { const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new TerminalLine(cols, ch, isWrapped); + return new BufferLine(cols, ch, isWrapped); } - private _data: CharData[]; + protected _data: CharData[]; public isWrapped = false; public length: number; @@ -35,7 +35,7 @@ export class TerminalLine { this.length = this._data.length; if (cols) { if (!ch) { - ch = TerminalLine.defaultCell; + ch = BufferLine.defaultCell; } for (let i = 0; i < cols; i++) { this.push(ch); // Note: the ctor ch is not cloned @@ -57,7 +57,6 @@ export class TerminalLine { // to be removed for typed array public pop(): CharData | undefined { - // TODO: unref here, change CharData to [typeof Attributes, ...] const data = this._data.pop(); this.length = this._data.length; return data; @@ -67,25 +66,15 @@ export class TerminalLine { public push(data: CharData): void { this._data.push(data); this.length = this._data.length; - // TODO: ref here } // to be removed for typed array public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { const removed = this._data.splice(start, deleteCount, ...items); this.length = this._data.length; - // TODO: ref new, unref old return removed; } - /** to be called when a line gets removed */ - public release(): void { - // TODO: unref here - } - public toArray(): CharData[] { - return this._data; - } - /** insert n cells ch at pos, right cells are lost (stable length) */ public insertCells(pos: number, n: number, ch: CharData): void { while (n--) { diff --git a/src/Types.ts b/src/Types.ts index ecec9a95..c567fa25 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -7,7 +7,6 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, import { IColorSet, IRenderer } from './renderer/Types'; import { IMouseZoneManager } from './ui/Types'; import { ICharset } from './core/Types'; -import { TerminalLine } from './TerminalLine'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -272,7 +271,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { } export interface IBuffer { - readonly lines: ICircularList; + readonly lines: ICircularList; ydisp: number; ybase: number; y: number; @@ -511,3 +510,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/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 2932dd5c..aa942f3e 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { ITerminal, ICircularList } from '../Types'; +import { ITerminal, ICircularList, IBufferLine } from '../Types'; import { C0 } from '../common/data/EscapeSequences'; -import { TerminalLine } from '../TerminalLine'; const enum Direction { UP = 'A', @@ -19,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, diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index d7cd1ce4..b05b2f8d 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -5,7 +5,8 @@ import { CircularList } from '../common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { TerminalLine } from '../TerminalLine'; +import { BufferLine } from '../TerminalLine'; +import { IBufferLine } from '../Types'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; @@ -14,13 +15,13 @@ describe('CharacterJoinerRegistry', () => { const terminal = new MockTerminal(); terminal.cols = 16; terminal.buffer = new MockBuffer(); - const lines = new CircularList(7); + 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 TerminalLine()); + 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)]); @@ -262,8 +263,8 @@ describe('CharacterJoinerRegistry', () => { type IPartialLineData = ([string] | [string, number]); -function lineData(data: IPartialLineData[]): TerminalLine { - const tline = new TerminalLine(); +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); diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index 2b50f5ac..dc9e95dd 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,7 +1,6 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { ITerminal } from '../Types'; +import { ITerminal, IBufferLine } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; -import { TerminalLine } from '../TerminalLine'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { @@ -116,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: TerminalLine, 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 @@ -141,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: TerminalLine, startCol: number): void { + private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void { let currentRangeIndex = 0; let currentRangeStarted = false; let currentStringIndex = 0; diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 9503a46c..ebce9028 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -8,12 +8,13 @@ import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { TerminalLine } from '../../TerminalLine'; +import { BufferLine } from '../../TerminalLine'; +import { IBufferLine } from '../../Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; let rowFactory: DomRendererRowFactory; - let lineData: TerminalLine; + let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); @@ -146,8 +147,8 @@ describe('DomRendererRowFactory', () => { return element.innerHTML; } - function createEmptyLineData(cols: number): TerminalLine { - const lineData = new TerminalLine(); + 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 1c259549..351055ef 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,7 @@ import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; import { FLAGS } from '../Types'; -import { TerminalLine } from '../../TerminalLine'; +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: TerminalLine, 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; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index a279b48b..b9bb0348 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -4,11 +4,10 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler, IBufferLine } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; -import { TerminalLine } from '../TerminalLine'; export class MockTerminal implements ITerminal { markers: IMarker[]; @@ -285,7 +284,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { export class MockBuffer implements IBuffer { isCursorInViewport: boolean; - lines: ICircularList; + lines: ICircularList; ydisp: number; ybase: number; hasScrollback: boolean; @@ -308,7 +307,7 @@ export class MockBuffer implements IBuffer { prevStop(x?: number): number { throw new Error('Method not implemented.'); } - setLines(lines: ICircularList): void { + setLines(lines: ICircularList): void { this.lines = lines; } } From 71f5965f8e43e4d81f48377e808c44937a1c6c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 17:55:04 +0200 Subject: [PATCH 19/27] move to BufferLine.ts --- src/Buffer.test.ts | 2 +- src/Buffer.ts | 2 +- src/{TerminalLine.test.ts => BufferLine.test.ts} | 2 +- src/{TerminalLine.ts => BufferLine.ts} | 0 src/InputHandler.ts | 2 +- src/Linkifier.test.ts | 2 +- src/SelectionManager.test.ts | 2 +- src/Terminal.test.ts | 2 +- src/Terminal.ts | 2 +- src/renderer/CharacterJoinerRegistry.test.ts | 2 +- src/renderer/dom/DomRendererRowFactory.test.ts | 2 +- 11 files changed, 10 insertions(+), 10 deletions(-) rename src/{TerminalLine.test.ts => BufferLine.test.ts} (98%) rename src/{TerminalLine.ts => BufferLine.ts} (100%) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index ded814de..6f417f8e 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -8,7 +8,7 @@ import { ITerminal } from './Types'; import { Buffer, DEFAULT_ATTR } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; -import { BufferLine } from './TerminalLine'; +import { BufferLine } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/Buffer.ts b/src/Buffer.ts index 40cbf29e..81b8a517 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -7,7 +7,7 @@ import { CircularList } from './common/CircularList'; import { CharData, ITerminal, IBuffer, IBufferLine } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine } from './TerminalLine'; +import { BufferLine } from './BufferLine'; export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; diff --git a/src/TerminalLine.test.ts b/src/BufferLine.test.ts similarity index 98% rename from src/TerminalLine.test.ts rename to src/BufferLine.test.ts index 19a4d4c7..1695f9aa 100644 --- a/src/TerminalLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine } from './TerminalLine'; +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'; diff --git a/src/TerminalLine.ts b/src/BufferLine.ts similarity index 100% rename from src/TerminalLine.ts rename to src/BufferLine.ts diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ca743575..a33acd93 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -13,7 +13,7 @@ import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { Disposable } from './common/Lifecycle'; -import { BufferLine } from './TerminalLine'; +import { BufferLine } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0f84d41c..eeaaeedc 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -9,7 +9,7 @@ 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 './TerminalLine'; +import { BufferLine } from './BufferLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 880a42d3..9359793d 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,7 +10,7 @@ import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { ITerminal, IBuffer, IBufferLine } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; -import { BufferLine } from './TerminalLine'; +import { BufferLine } from './BufferLine'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 4f765151..83ab9a68 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -7,7 +7,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; -import { BufferLine } from './TerminalLine'; +import { BufferLine } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/Terminal.ts b/src/Terminal.ts index 56d98afd..07301567 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,7 +52,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; -import { BufferLine } from './TerminalLine'; +import { BufferLine } from './BufferLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index b05b2f8d..bb7a2c5d 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -5,7 +5,7 @@ import { CircularList } from '../common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { BufferLine } from '../TerminalLine'; +import { BufferLine } from '../BufferLine'; import { IBufferLine } from '../Types'; describe('CharacterJoinerRegistry', () => { diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index ebce9028..17b1b938 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -8,7 +8,7 @@ import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { BufferLine } from '../../TerminalLine'; +import { BufferLine } from '../../BufferLine'; import { IBufferLine } from '../../Types'; describe('DomRendererRowFactory', () => { From a1421b151ecf847c617019e44f2c6405a3b6b302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 17:58:37 +0200 Subject: [PATCH 20/27] cleanup BufferLine.ts --- src/BufferLine.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 51e0148b..76031127 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -7,22 +7,10 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; /** * Class representing a terminal line. - * Currently the class is a thin proxy to `CharData[]`. - * Once the storages are in place it will proxy access to - * typed array based line data. - * TODO: - * - move Buffer.translateBufferLineToString here? - * - next steps towards typed array: - * - create ITerminalLine interface w'o length methods - * - resize method - * - replace all external push/pop/splice accesses - * - fixed length - * - remove push/pop/splice - * - implement typed array alternative once string is removed from CharData */ export class BufferLine implements IBufferLine { static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - static blankLine(cols: number, attr: number, isWrapped?: boolean): BufferLine { + 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); } @@ -52,23 +40,19 @@ export class BufferLine implements IBufferLine { public set(index: number, data: CharData): void { this._data[index] = data; - // TODO: unref old, ref new } - // to be removed for typed array public pop(): CharData | undefined { const data = this._data.pop(); this.length = this._data.length; return data; } - // to be removed for typed array public push(data: CharData): void { this._data.push(data); this.length = this._data.length; } - // to be removed for typed array public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { const removed = this._data.splice(start, deleteCount, ...items); this.length = this._data.length; From 276e65fa0bf45805e450a9afe5e3ac0fcbd6e7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 18:10:18 +0200 Subject: [PATCH 21/27] move erase methods to InputHandler --- src/InputHandler.test.ts | 16 +++++----- src/InputHandler.ts | 63 ++++++++++++++++++++++++++++++++++++++++ src/Terminal.ts | 39 ------------------------- src/Types.ts | 6 ++-- 4 files changed, 74 insertions(+), 50 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 4774c105..0d5d9b74 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -261,13 +261,13 @@ describe('InputHandler', () => { function eraseInLine(params: number[]): void { switch (params[0]) { case 0: - term.eraseRight(term.buffer.x, term.buffer.y); + inputHandler.eraseRight(term.buffer.x, term.buffer.y); break; case 1: - term.eraseLeft(term.buffer.x, term.buffer.y); + inputHandler.eraseLeft(term.buffer.x, term.buffer.y); break; case 2: - term.eraseLine(term.buffer.y); + inputHandler.eraseLine(term.buffer.y); break; } } @@ -321,22 +321,22 @@ describe('InputHandler', () => { let j; switch (params[0]) { case 0: - termOld.eraseRight(termOld.buffer.x, termOld.buffer.y); + inputHandlerOld.eraseRight(termOld.buffer.x, termOld.buffer.y); j = termOld.buffer.y + 1; for (; j < termOld.rows; j++) { - termOld.eraseLine(j); + inputHandlerOld.eraseLine(j); } break; case 1: - termOld.eraseLeft(termOld.buffer.x, termOld.buffer.y); + inputHandlerOld.eraseLeft(termOld.buffer.x, termOld.buffer.y); j = termOld.buffer.y; while (j--) { - termOld.eraseLine(j); + inputHandlerOld.eraseLine(j); } break; case 2: j = termOld.rows; - while (j--) termOld.eraseLine(j); + while (j--) inputHandlerOld.eraseLine(j); break; case 3: // Clear scrollback (everything not in viewport) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a33acd93..48849542 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1986,4 +1986,67 @@ export class InputHandler extends Disposable implements IInputHandler { public setgLevel(level: number): void { this._terminal.setgLevel(level); // TODO: save to move from terminal? } + + + + + + + + + + + + + + + + + + + + + + + + + + /** + * 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. + */ + // FIXME: decide whether to remove from Terminal + 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. + */ + // FIXME: decide whether to remove from Terminal + 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. + */ + // FIXME: decide whether to remove from Terminal + public eraseLine(y: number): void { + this.eraseRight(0, y); + } } diff --git a/src/Terminal.ts b/src/Terminal.ts index 07301567..3a0ce0ed 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1709,36 +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. - */ - // FIXME: decide whether to remove from Terminal - public eraseRight(x: number, y: number): void { - const line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - line.replaceCells(x, this.cols, [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - 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. - */ - // FIXME: decide whether to remove from Terminal - public eraseLeft(x: number, y: number): void { - const line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - line.replaceCells(0, x + 1, [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - this.updateRange(y); - } - /** * Clear the entire buffer, making the prompt line the new first line. */ @@ -1759,15 +1729,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.emit('scroll', this.buffer.ydisp); } - /** - * Erase all content in the given line - * @param y The line to erase all of its contents. - */ - // FIXME: decide whether to remove from Terminal - public eraseLine(y: number): void { - this.eraseRight(0, y); - } - /** * If cur return the back color xterm feature attribute. Else return default attribute. * @param cur diff --git a/src/Types.ts b/src/Types.ts index c567fa25..afd73603 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -71,9 +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; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -115,6 +112,9 @@ export interface ICompositionHelper { export interface IInputHandler { parse(data: string): void; print(data: string, start: number, end: number): void; + eraseRight(x: number, y: number): void; + eraseLine(y: number): void; + eraseLeft(x: number, y: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; From 32231dcd4107e94313372c2315c7c2e0645bf41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 20:27:52 +0200 Subject: [PATCH 22/27] remove static DefaultCell --- src/BufferLine.test.ts | 6 +++--- src/BufferLine.ts | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 1695f9aa..2018554b 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine } from './BufferLine'; +import { BufferLine, DEFAULT_CELL } 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'; @@ -22,11 +22,11 @@ describe('BufferLine', function(): void { chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(TestBufferLine.defaultCell); + chai.expect(line.pop()).eql(DEFAULT_CELL); chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(TestBufferLine.defaultCell); + chai.expect(line.pop()).eql(DEFAULT_CELL); chai.expect(line.isWrapped).equals(true); line = new TestBufferLine(10, [123, 'a', 456, 789], true); chai.expect(line.length).equals(10); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 6349467f..e0879b72 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -5,13 +5,12 @@ import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; -export const defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; +export const DEFAULT_CELL: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; /** * Class representing a terminal line. */ export class BufferLine implements IBufferLine { - static defaultCell: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; 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); @@ -25,7 +24,7 @@ export class BufferLine implements IBufferLine { this.length = this._data.length; if (cols) { if (!ch) { - ch = BufferLine.defaultCell; + ch = DEFAULT_CELL; } for (let i = 0; i < cols; i++) { this.push(ch); // Note: the ctor ch is not cloned From a74ca25b0962294b635ab234b76d2dd63856ff43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 20:29:23 +0200 Subject: [PATCH 23/27] cleanup spaces and comments in InputHandler --- src/InputHandler.ts | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 48849542..b5fbaa3a 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1987,36 +1987,11 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.setgLevel(level); // TODO: save to move from terminal? } - - - - - - - - - - - - - - - - - - - - - - - - - /** + /** * 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. */ - // FIXME: decide whether to remove from Terminal public eraseRight(x: number, y: number): void { const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); if (!line) { @@ -2031,7 +2006,6 @@ export class InputHandler extends Disposable implements IInputHandler { * @param x The column from which to start erasing to the start of the line. * @param y The line in which to operate. */ - // FIXME: decide whether to remove from Terminal public eraseLeft(x: number, y: number): void { const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); if (!line) { @@ -2041,11 +2015,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(y); } - /** + /** * Erase all content in the given line * @param y The line to erase all of its contents. */ - // FIXME: decide whether to remove from Terminal public eraseLine(y: number): void { this.eraseRight(0, y); } From 11df1e1d80b5b9833bd39c59bb0a06d4cab13157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 21:03:30 +0200 Subject: [PATCH 24/27] make sure DEFAULT_CELL gets not overwritten by accident --- src/BufferLine.test.ts | 21 +++++++++++++++++++++ src/BufferLine.ts | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 2018554b..bf4f5a2d 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -94,4 +94,25 @@ describe('BufferLine', function(): void { 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]]); }); + it('DEFAULT_CELL is not affected by cell writes', function(): void { + // get default values: + const attr = DEFAULT_CELL[CHAR_DATA_ATTR_INDEX]; + const char = DEFAULT_CELL[CHAR_DATA_CHAR_INDEX]; + const width = DEFAULT_CELL[CHAR_DATA_WIDTH_INDEX]; + const code = DEFAULT_CELL[CHAR_DATA_CODE_INDEX]; + // create a line with DEFAULT_CELL + const line = new TestBufferLine(3); + // alter first cell only + const first = line.get(0); + // this is bad - never edit a cell after a get!!!! (needs to be fixed in InputHandler.print) + // Note this is currently granted in the codebase by the way + // a blankLine was/is created - all cells point to the same + // CharData object + // we test here, that this unique blankLine object is not + // pointing to the DEFAULT_CELL object + first[CHAR_DATA_ATTR_INDEX] = 123456789; + chai.expect(line.toArray()).eql([[123456789, char, width, code], [123456789, char, width, code], [123456789, char, width, code]]); + chai.expect(DEFAULT_CELL[CHAR_DATA_ATTR_INDEX]).equals(attr); + chai.expect(DEFAULT_CELL[CHAR_DATA_ATTR_INDEX]).not.equals(123456789); + }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index e0879b72..a57bdf74 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -24,10 +24,10 @@ export class BufferLine implements IBufferLine { this.length = this._data.length; if (cols) { if (!ch) { - ch = DEFAULT_CELL; + ch = [DEFAULT_CELL[0], DEFAULT_CELL[1], DEFAULT_CELL[2], DEFAULT_CELL[3]]; } for (let i = 0; i < cols; i++) { - this.push(ch); // Note: the ctor ch is not cloned + this.push(ch); // Note: the ctor ch is not cloned (resembles old behavior) } } if (isWrapped) { @@ -79,7 +79,7 @@ export class BufferLine implements IBufferLine { /** 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 + this.set(start++, fill); // Note: fill is not cloned (resembles old behavior) } } } From 0d49d1f64dde85c8ba7c05301f8c3f721f1ec596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 30 Aug 2018 21:12:55 +0200 Subject: [PATCH 25/27] set DEFAULT_CELL explicit in ctor and remove the global const value --- src/BufferLine.test.ts | 27 +++------------------------ src/BufferLine.ts | 4 +--- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index bf4f5a2d..61dfe543 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, DEFAULT_CELL } from './BufferLine'; +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'; @@ -22,11 +22,11 @@ describe('BufferLine', function(): void { chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql(DEFAULT_CELL); + 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(DEFAULT_CELL); + 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); @@ -94,25 +94,4 @@ describe('BufferLine', function(): void { 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]]); }); - it('DEFAULT_CELL is not affected by cell writes', function(): void { - // get default values: - const attr = DEFAULT_CELL[CHAR_DATA_ATTR_INDEX]; - const char = DEFAULT_CELL[CHAR_DATA_CHAR_INDEX]; - const width = DEFAULT_CELL[CHAR_DATA_WIDTH_INDEX]; - const code = DEFAULT_CELL[CHAR_DATA_CODE_INDEX]; - // create a line with DEFAULT_CELL - const line = new TestBufferLine(3); - // alter first cell only - const first = line.get(0); - // this is bad - never edit a cell after a get!!!! (needs to be fixed in InputHandler.print) - // Note this is currently granted in the codebase by the way - // a blankLine was/is created - all cells point to the same - // CharData object - // we test here, that this unique blankLine object is not - // pointing to the DEFAULT_CELL object - first[CHAR_DATA_ATTR_INDEX] = 123456789; - chai.expect(line.toArray()).eql([[123456789, char, width, code], [123456789, char, width, code], [123456789, char, width, code]]); - chai.expect(DEFAULT_CELL[CHAR_DATA_ATTR_INDEX]).equals(attr); - chai.expect(DEFAULT_CELL[CHAR_DATA_ATTR_INDEX]).not.equals(123456789); - }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index a57bdf74..639049e2 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -5,8 +5,6 @@ import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; -export const DEFAULT_CELL: CharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - /** * Class representing a terminal line. */ @@ -24,7 +22,7 @@ export class BufferLine implements IBufferLine { this.length = this._data.length; if (cols) { if (!ch) { - ch = [DEFAULT_CELL[0], DEFAULT_CELL[1], DEFAULT_CELL[2], DEFAULT_CELL[3]]; + 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) From 999fce4e52ae3b235118a92afe40b8845f56f69f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 30 Aug 2018 16:22:30 -0700 Subject: [PATCH 26/27] Remove test only functions from InputHandler.s --- src/InputHandler.test.ts | 153 +++++++++++++++++++++++++-------------- src/InputHandler.ts | 38 +--------- src/Types.ts | 3 - 3 files changed, 98 insertions(+), 96 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 0d5d9b74..0db6c04f 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -10,6 +10,95 @@ import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX } 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', () => { const terminal = new MockInputHandlingTerminal(); @@ -257,20 +346,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); - - function eraseInLine(params: number[]): void { - switch (params[0]) { - case 0: - inputHandler.eraseRight(term.buffer.x, term.buffer.y); - break; - case 1: - inputHandler.eraseLeft(term.buffer.x, term.buffer.y); - break; - case 2: - inputHandler.eraseLine(term.buffer.y); - break; - } - } + const oldInputHandler = new OldInputHandler(term); // fill 6 lines to test 3 different states inputHandler.parse(Array(term.cols + 1).join('a')); @@ -283,7 +359,7 @@ describe('InputHandler', () => { // params[0] - right erase term.buffer.y = 0; term.buffer.x = 70; - eraseInLine([0]); + oldInputHandler.eraseInLine([0]); expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' '); term.buffer.y = 1; term.buffer.x = 70; @@ -293,7 +369,7 @@ describe('InputHandler', () => { // params[1] - left erase term.buffer.y = 2; term.buffer.x = 70; - eraseInLine([1]); + oldInputHandler.eraseInLine([1]); expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa'); term.buffer.y = 3; term.buffer.x = 70; @@ -303,7 +379,7 @@ describe('InputHandler', () => { // params[1] - left erase term.buffer.y = 4; term.buffer.x = 70; - eraseInLine([2]); + oldInputHandler.eraseInLine([2]); expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' ')); term.buffer.y = 5; term.buffer.x = 70; @@ -313,45 +389,10 @@ describe('InputHandler', () => { }); it('eraseInDisplay', function(): void { const termOld = new Terminal(); - const inputHandlerOld = new InputHandler(termOld); + const inputHandlerOld = new OldInputHandler(termOld); const termNew = new Terminal(); const inputHandlerNew = new InputHandler(termNew); - function eraseInDisplay(params: number[]): void { - let j; - switch (params[0]) { - case 0: - inputHandlerOld.eraseRight(termOld.buffer.x, termOld.buffer.y); - j = termOld.buffer.y + 1; - for (; j < termOld.rows; j++) { - inputHandlerOld.eraseLine(j); - } - break; - case 1: - inputHandlerOld.eraseLeft(termOld.buffer.x, termOld.buffer.y); - j = termOld.buffer.y; - while (j--) { - inputHandlerOld.eraseLine(j); - } - break; - case 2: - j = termOld.rows; - while (j--) inputHandlerOld.eraseLine(j); - break; - case 3: - // Clear scrollback (everything not in viewport) - const scrollBackSize = termOld.buffer.lines.length - termOld.rows; - if (scrollBackSize > 0) { - termOld.buffer.lines.trimStart(scrollBackSize); - termOld.buffer.ybase = Math.max(termOld.buffer.ybase - scrollBackSize, 0); - termOld.buffer.ydisp = Math.max(termOld.buffer.ydisp - scrollBackSize, 0); - // Force a scroll event to refresh viewport - termOld.emit('scroll', 0); - } - break; - } - } - // 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')); @@ -363,7 +404,7 @@ describe('InputHandler', () => { // params [0] - right and below erase termOld.buffer.y = 5; termOld.buffer.x = 40; - eraseInDisplay([0]); + inputHandlerOld.eraseInDisplay([0]); termNew.buffer.y = 5; termNew.buffer.x = 40; inputHandlerNew.eraseInDisplay([0]); @@ -380,7 +421,7 @@ describe('InputHandler', () => { // params [1] - left and above termOld.buffer.y = 5; termOld.buffer.x = 40; - eraseInDisplay([1]); + inputHandlerOld.eraseInDisplay([1]); termNew.buffer.y = 5; termNew.buffer.x = 40; inputHandlerNew.eraseInDisplay([1]); @@ -397,7 +438,7 @@ describe('InputHandler', () => { // params [2] - whole screen termOld.buffer.y = 5; termOld.buffer.x = 40; - eraseInDisplay([2]); + inputHandlerOld.eraseInDisplay([2]); termNew.buffer.y = 5; termNew.buffer.x = 40; inputHandlerNew.eraseInDisplay([2]); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index b5fbaa3a..3c6aae0f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -117,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(); @@ -1986,40 +1986,4 @@ export class InputHandler extends Disposable implements IInputHandler { public setgLevel(level: number): void { this._terminal.setgLevel(level); // TODO: save to move from terminal? } - - /** - * 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); - } } diff --git a/src/Types.ts b/src/Types.ts index afd73603..5ea2024a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -112,9 +112,6 @@ export interface ICompositionHelper { export interface IInputHandler { parse(data: string): void; print(data: string, start: number, end: number): void; - eraseRight(x: number, y: number): void; - eraseLine(y: number): void; - eraseLeft(x: number, y: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; From a194263183dd7523b370951defc209e3c75a368f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 31 Aug 2018 12:02:23 -0700 Subject: [PATCH 27/27] Document addDisposableListener Part of #1642 --- typings/xterm.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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; /**