diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 4ff359d0..6d47a75f 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -4,7 +4,8 @@ */ import * as Strings from './Strings'; -import { ITerminal, IBuffer } from './Types'; +import { ITerminal } from './Types'; +import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { addDisposableDomListener } from 'browser/Lifecycle'; diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts deleted file mode 100644 index 101912bb..00000000 --- a/src/Buffer.test.ts +++ /dev/null @@ -1,1403 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert, expect } from 'chai'; -import { ITerminal } from './Types'; -import { Buffer } from './Buffer'; -import { CircularList } from 'common/CircularList'; -import { MockTerminal, TestTerminal } from './TestUtils.test'; -import { BufferLine, CellData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; - -const INIT_COLS = 80; -const INIT_ROWS = 24; - -describe('Buffer', () => { - let terminal: ITerminal; - let buffer: Buffer; - - beforeEach(() => { - terminal = new MockTerminal(); - (terminal as any).cols = INIT_COLS; - (terminal as any).rows = INIT_ROWS; - terminal.options.scrollback = 1000; - buffer = new Buffer(terminal, true); - }); - - describe('constructor', () => { - it('should create a CircularList with max length equal to rows + scrollback, for its lines', () => { - assert.instanceOf(buffer.lines, CircularList); - assert.equal(buffer.lines.maxLength, terminal.rows + terminal.options.scrollback); - }); - it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => { - assert.equal(buffer.scrollBottom, terminal.rows - 1); - }); - }); - - describe('fillViewportRows', () => { - it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData(); - 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).loadCell(x, new CellData()).getAsCharData(), blankLineChar); - } - } - }); - }); - - describe('getWrappedRangeForLine', () => { - describe('non-wrapped', () => { - it('should return a single row for the first row', () => { - buffer.fillViewportRows(); - assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 }); - }); - it('should return a single row for a middle row', () => { - buffer.fillViewportRows(); - assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 }); - }); - it('should return a single row for the last row', () => { - buffer.fillViewportRows(); - assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 }); - }); - }); - describe('wrapped', () => { - it('should return a range for the first row', () => { - buffer.fillViewportRows(); - 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; - 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; - 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; - 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; - 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; - 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; - assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); - }); - }); - }); - - describe('resize', () => { - describe('column size is reduced', () => { - it('should trim the data in the buffer', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS / 2, INIT_ROWS); - assert.equal(buffer.lines.length, INIT_ROWS); - for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS / 2); - } - }); - }); - - describe('column size is increased', () => { - it('should add pad columns', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS + 10, INIT_ROWS); - assert.equal(buffer.lines.length, INIT_ROWS); - for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS + 10); - } - }); - }); - - describe('row size reduced', () => { - it('should trim blank lines from the end', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS, INIT_ROWS - 10); - assert.equal(buffer.lines.length, INIT_ROWS - 10); - }); - - it('should move the viewport down when it\'s at the end', () => { - buffer.fillViewportRows(); - // Set cursor y to have 5 blank lines below it - buffer.y = INIT_ROWS - 5 - 1; - buffer.resize(INIT_COLS, INIT_ROWS - 10); - // Trim 5 rows - assert.equal(buffer.lines.length, INIT_ROWS - 5); - // Shift the viewport down 5 rows - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 5); - }); - - describe('no scrollback', () => { - it('should trim from the top of the buffer when the cursor reaches the bottom', () => { - terminal.options.scrollback = 0; - buffer = new Buffer(terminal, true); - assert.equal(buffer.lines.maxLength, INIT_ROWS); - buffer.y = INIT_ROWS - 1; - buffer.fillViewportRows(); - let chData = buffer.lines.get(5).loadCell(0, new CellData()).getAsCharData(); - chData[1] = 'a'; - buffer.lines.get(5).setCell(0, CellData.fromCharData(chData)); - chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getAsCharData(); - chData[1] = 'b'; - buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData)); - buffer.resize(INIT_COLS, INIT_ROWS - 5); - assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).getAsCharData()[1], 'a'); - assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).getAsCharData()[1], 'b'); - }); - }); - }); - - describe('row size increased', () => { - describe('empty buffer', () => { - it('should add blank lines to end', () => { - buffer.fillViewportRows(); - assert.equal(buffer.ydisp, 0); - buffer.resize(INIT_COLS, INIT_ROWS + 10); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.lines.length, INIT_ROWS + 10); - }); - }); - - describe('filled buffer', () => { - it('should show more of the buffer above', () => { - buffer.fillViewportRows(); - // Create 10 extra blank lines - for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - // Set cursor to the bottom of the buffer - buffer.y = INIT_ROWS - 1; - // Scroll down 10 lines - buffer.ybase = 10; - buffer.ydisp = 10; - assert.equal(buffer.lines.length, INIT_ROWS + 10); - buffer.resize(INIT_COLS, INIT_ROWS + 5); - // Should be should 5 more lines - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 5); - // Should not trim the buffer - assert.equal(buffer.lines.length, INIT_ROWS + 10); - }); - - it('should show more of the buffer below when the viewport is at the top of the buffer', () => { - buffer.fillViewportRows(); - // Create 10 extra blank lines - for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - // Set cursor to the bottom of the buffer - buffer.y = INIT_ROWS - 1; - // Scroll down 10 lines - buffer.ybase = 10; - buffer.ydisp = 0; - assert.equal(buffer.lines.length, INIT_ROWS + 10); - buffer.resize(INIT_COLS, INIT_ROWS + 5); - // The viewport should remain at the top - assert.equal(buffer.ydisp, 0); - // The buffer ybase should move up 5 lines - assert.equal(buffer.ybase, 5); - // Should not trim the buffer - assert.equal(buffer.lines.length, INIT_ROWS + 10); - }); - }); - }); - - describe('row and column increased', () => { - it('should resize properly', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS + 5, INIT_ROWS + 5); - assert.equal(buffer.lines.length, INIT_ROWS + 5); - for (let i = 0; i < INIT_ROWS + 5; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS + 5); - } - }); - }); - - describe('reflow', () => { - it('should not wrap empty lines', () => { - buffer.fillViewportRows(); - assert.equal(buffer.lines.length, INIT_ROWS); - buffer.resize(INIT_COLS - 5, INIT_ROWS); - assert.equal(buffer.lines.length, INIT_ROWS); - }); - it('should shrink row length', () => { - buffer.fillViewportRows(); - buffer.resize(5, 10); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).length, 5); - assert.equal(buffer.lines.get(1).length, 5); - assert.equal(buffer.lines.get(2).length, 5); - assert.equal(buffer.lines.get(3).length, 5); - assert.equal(buffer.lines.get(4).length, 5); - assert.equal(buffer.lines.get(5).length, 5); - assert.equal(buffer.lines.get(6).length, 5); - assert.equal(buffer.lines.get(7).length, 5); - assert.equal(buffer.lines.get(8).length, 5); - assert.equal(buffer.lines.get(9).length, 5); - }); - it('should wrap and unwrap lines', () => { - buffer.fillViewportRows(); - buffer.resize(5, 10); - const firstLine = buffer.lines.get(0); - for (let i = 0; i < 5; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - firstLine.set(i, [null, char, 1, code]); - } - buffer.y = 1; - assert.equal(buffer.lines.get(0).length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); - buffer.resize(1, 10); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'a'); - assert.equal(buffer.lines.get(1).translateToString(), 'b'); - assert.equal(buffer.lines.get(2).translateToString(), 'c'); - assert.equal(buffer.lines.get(3).translateToString(), 'd'); - assert.equal(buffer.lines.get(4).translateToString(), 'e'); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - assert.equal(buffer.lines.get(6).translateToString(), ' '); - assert.equal(buffer.lines.get(7).translateToString(), ' '); - assert.equal(buffer.lines.get(8).translateToString(), ' '); - assert.equal(buffer.lines.get(9).translateToString(), ' '); - buffer.resize(5, 10); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); - assert.equal(buffer.lines.get(1).translateToString(), ' '); - assert.equal(buffer.lines.get(2).translateToString(), ' '); - assert.equal(buffer.lines.get(3).translateToString(), ' '); - assert.equal(buffer.lines.get(4).translateToString(), ' '); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - assert.equal(buffer.lines.get(6).translateToString(), ' '); - assert.equal(buffer.lines.get(7).translateToString(), ' '); - assert.equal(buffer.lines.get(8).translateToString(), ' '); - assert.equal(buffer.lines.get(9).translateToString(), ' '); - }); - it('should discard parts of wrapped lines that go out of the scrollback', () => { - buffer.fillViewportRows(); - terminal.options.scrollback = 1; - buffer.resize(10, 5); - const lastLine = buffer.lines.get(3); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - lastLine.set(i, [null, char, 1, code]); - } - assert.equal(buffer.lines.length, 5); - buffer.y = 4; - buffer.resize(2, 5); - assert.equal(buffer.y, 4); - assert.equal(buffer.ybase, 1); - assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - buffer.resize(1, 5); - assert.equal(buffer.y, 4); - assert.equal(buffer.ybase, 1); - assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), 'f'); - assert.equal(buffer.lines.get(1).translateToString(), 'g'); - assert.equal(buffer.lines.get(2).translateToString(), 'h'); - assert.equal(buffer.lines.get(3).translateToString(), 'i'); - assert.equal(buffer.lines.get(4).translateToString(), 'j'); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - buffer.resize(10, 5); - assert.equal(buffer.y, 1); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'fghij '); - assert.equal(buffer.lines.get(1).translateToString(), ' '); - assert.equal(buffer.lines.get(2).translateToString(), ' '); - assert.equal(buffer.lines.get(3).translateToString(), ' '); - assert.equal(buffer.lines.get(4).translateToString(), ' '); - }); - it('should remove the correct amount of rows when reflowing larger', () => { - // This is a regression test to ensure that successive wrapped lines that are getting - // 3+ lines removed on a reflow actually remove the right lines - buffer.fillViewportRows(); - buffer.resize(10, 10); - buffer.y = 2; - const firstLine = buffer.lines.get(0); - const secondLine = buffer.lines.get(1); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - firstLine.set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = '0'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - secondLine.set(i, [null, char, 1, code]); - } - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - for (let i = 2; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - buffer.resize(2, 10); - assert.equal(buffer.ybase, 1); - assert.equal(buffer.lines.length, 11); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), '01'); - assert.equal(buffer.lines.get(6).translateToString(), '23'); - assert.equal(buffer.lines.get(7).translateToString(), '45'); - assert.equal(buffer.lines.get(8).translateToString(), '67'); - assert.equal(buffer.lines.get(9).translateToString(), '89'); - assert.equal(buffer.lines.get(10).translateToString(), ' '); - buffer.resize(10, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - for (let i = 2; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - }); - it('should transfer combined char data over to reflowed lines', () => { - buffer.fillViewportRows(); - buffer.resize(4, 3); - buffer.y = 2; - const firstLine = buffer.lines.get(0); - firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]); - firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]); - firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]); - firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - assert.equal(buffer.lines.length, 3); - assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); - assert.equal(buffer.lines.get(1).translateToString(), ' '); - buffer.resize(2, 3); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); - }); - it('should adjust markers when reflowing', () => { - buffer.fillViewportRows(); - buffer.resize(10, 16); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(0).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = '0'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(1).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = 'k'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(2).set(i, [null, char, 1, code]); - } - buffer.y = 3; - // Buffer: - // abcdefghij - // 0123456789 - // abcdefghij - const firstMarker = buffer.addMarker(0); - const secondMarker = buffer.addMarker(1); - const thirdMarker = buffer.addMarker(2); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(firstMarker.line, 0); - assert.equal(secondMarker.line, 1); - assert.equal(thirdMarker.line, 2); - buffer.resize(2, 16); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), '01'); - assert.equal(buffer.lines.get(6).translateToString(), '23'); - assert.equal(buffer.lines.get(7).translateToString(), '45'); - assert.equal(buffer.lines.get(8).translateToString(), '67'); - assert.equal(buffer.lines.get(9).translateToString(), '89'); - assert.equal(buffer.lines.get(10).translateToString(), 'kl'); - assert.equal(buffer.lines.get(11).translateToString(), 'mn'); - assert.equal(buffer.lines.get(12).translateToString(), 'op'); - assert.equal(buffer.lines.get(13).translateToString(), 'qr'); - assert.equal(buffer.lines.get(14).translateToString(), 'st'); - assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); - assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); - assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); - buffer.resize(10, 16); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); - assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); - assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); - assert.equal(firstMarker.isDisposed, false); - assert.equal(secondMarker.isDisposed, false); - assert.equal(thirdMarker.isDisposed, false); - }); - it('should dispose markers whose rows are trimmed during a reflow', () => { - buffer.fillViewportRows(); - terminal.options.scrollback = 1; - buffer.resize(10, 11); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(0).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = '0'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(1).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = 'k'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(2).set(i, [null, char, 1, code]); - } - buffer.y = 10; - // Buffer: - // abcdefghij - // 0123456789 - // abcdefghij - const firstMarker = buffer.addMarker(0); - const secondMarker = buffer.addMarker(1); - const thirdMarker = buffer.addMarker(2); - buffer.y = 3; - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(firstMarker.line, 0); - assert.equal(secondMarker.line, 1); - assert.equal(thirdMarker.line, 2); - buffer.resize(2, 11); - assert.equal(buffer.lines.get(0).translateToString(), 'ij'); - assert.equal(buffer.lines.get(1).translateToString(), '01'); - assert.equal(buffer.lines.get(2).translateToString(), '23'); - assert.equal(buffer.lines.get(3).translateToString(), '45'); - assert.equal(buffer.lines.get(4).translateToString(), '67'); - assert.equal(buffer.lines.get(5).translateToString(), '89'); - assert.equal(buffer.lines.get(6).translateToString(), 'kl'); - assert.equal(buffer.lines.get(7).translateToString(), 'mn'); - assert.equal(buffer.lines.get(8).translateToString(), 'op'); - assert.equal(buffer.lines.get(9).translateToString(), 'qr'); - assert.equal(buffer.lines.get(10).translateToString(), 'st'); - assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4'); - assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped'); - assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); - assert.equal(secondMarker.isDisposed, false); - assert.equal(thirdMarker.isDisposed, false); - buffer.resize(10, 11); - assert.equal(buffer.lines.get(0).translateToString(), 'ij '); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(secondMarker.line, 1, 'second marker should be restored'); - assert.equal(thirdMarker.line, 2, 'third marker should be restored'); - }); - it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { - buffer.fillViewportRows(); - buffer.resize(4, 10); - buffer.y = 2; - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).isWrapped = true; - // Buffer: - // "ab " (wrapped) - // "cd" - buffer.resize(5, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), 'ab c'); - assert.equal(buffer.lines.get(1).translateToString(false), 'd '); - buffer.resize(6, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), 'ab cd'); - assert.equal(buffer.lines.get(1).translateToString(false), ' '); - }); - it('should wrap wide characters correctly when reflowing larger', () => { - buffer.fillViewportRows(); - buffer.resize(12, 10); - buffer.y = 2; - for (let i = 0; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - } - for (let i = 2; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - } - for (let i = 1; i < 12; i += 2) { - buffer.lines.get(0).set(i, [null, '', 0, undefined]); - buffer.lines.get(1).set(i, [null, '', 0, undefined]); - } - buffer.lines.get(1).isWrapped = true; - // Buffer: - // 汉语汉语汉语 (wrapped) - // 汉语汉语汉语 - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); - buffer.resize(13, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语 '); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(false), '汉语汉语汉语 '); - buffer.resize(14, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语汉'); - assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); - }); - it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { - buffer.fillViewportRows(); - buffer.resize(4, 10); - buffer.y = 2; - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).isWrapped = true; - // Buffer: - // "ab " (wrapped) - // "cd" - buffer.resize(3, 10); - assert.equal(buffer.y, 2); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(false), 'ab '); - assert.equal(buffer.lines.get(1).translateToString(false), ' cd'); - buffer.resize(2, 10); - assert.equal(buffer.y, 3); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(false), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(false), ' '); - assert.equal(buffer.lines.get(2).translateToString(false), 'cd'); - }); - it('should wrap wide characters correctly when reflowing smaller', () => { - buffer.fillViewportRows(); - buffer.resize(12, 10); - buffer.y = 2; - for (let i = 0; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - } - for (let i = 2; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - } - for (let i = 1; i < 12; i += 2) { - buffer.lines.get(0).set(i, [null, '', 0, undefined]); - buffer.lines.get(1).set(i, [null, '', 0, undefined]); - } - buffer.lines.get(1).isWrapped = true; - // Buffer: - // 汉语汉语汉语 (wrapped) - // 汉语汉语汉语 - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); - buffer.resize(11, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); - buffer.resize(10, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); - buffer.resize(9, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); - buffer.resize(8, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); - buffer.resize(7, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); - buffer.resize(6, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); - }); - - describe('reflowLarger cases', () => { - beforeEach(() => { - // Setup buffer state: - // 'ab' - // 'cd' (wrapped) - // 'ef' - // 'gh' (wrapped) - // 'ij' - // 'kl' (wrapped) - // ' ' - // ' ' - // ' ' - // ' ' - buffer.fillViewportRows(); - buffer.resize(2, 10); - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).isWrapped = true; - buffer.lines.get(2).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); - buffer.lines.get(2).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); - buffer.lines.get(3).set(0, [null, 'g', 1, 'g'.charCodeAt(0)]); - buffer.lines.get(3).set(1, [null, 'h', 1, 'h'.charCodeAt(0)]); - buffer.lines.get(3).isWrapped = true; - buffer.lines.get(4).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); - buffer.lines.get(4).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); - buffer.lines.get(5).set(0, [null, 'k', 1, 'k'.charCodeAt(0)]); - buffer.lines.get(5).set(1, [null, 'l', 1, 'l'.charCodeAt(0)]); - buffer.lines.get(5).isWrapped = true; - }); - describe('viewport not yet filled', () => { - it('should move the cursor up and add empty lines', () => { - buffer.y = 6; - buffer.resize(4, 10); - assert.equal(buffer.y, 3); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); - for (let i = 3; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('viewport filled, scrollback remaining', () => { - beforeEach(() => { - buffer.y = 9; - }); - describe('ybase === 0', () => { - it('should move the cursor up and add empty lines', () => { - buffer.resize(4, 10); - assert.equal(buffer.y, 6); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); - for (let i = 3; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('ybase !== 0', () => { - beforeEach(() => { - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should adjust the viewport and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 7); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should keep ydisp at the same value', () => { - buffer.ydisp = 5; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - describe('viewport filled, no scrollback remaining', () => { - // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported - describe('ybase !== 0', () => { - beforeEach(() => { - terminal.options.scrollback = 10; - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.y = 9; - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should trim lines and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 7); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should trim lines and not change ydisp', () => { - buffer.ydisp = 5; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - }); - describe('reflowSmaller cases', () => { - beforeEach(() => { - // Setup buffer state: - // 'abcd' - // 'efgh' (wrapped) - // 'ijkl' - // ' ' - // ' ' - // ' ' - // ' ' - // ' ' - // ' ' - // ' ' - buffer.fillViewportRows(); - buffer.resize(4, 10); - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(0).set(2, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(0).set(3, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); - buffer.lines.get(1).set(2, [null, 'g', 1, 'g'.charCodeAt(0)]); - buffer.lines.get(1).set(3, [null, 'h', 1, 'h'.charCodeAt(0)]); - buffer.lines.get(2).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); - buffer.lines.get(2).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); - buffer.lines.get(2).set(2, [null, 'k', 1, 'k'.charCodeAt(0)]); - buffer.lines.get(2).set(3, [null, 'l', 1, 'l'.charCodeAt(0)]); - }); - describe('viewport not yet filled', () => { - it('should move the cursor down', () => { - buffer.y = 3; - buffer.resize(2, 10); - assert.equal(buffer.y, 6); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), 'kl'); - for (let i = 6; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [1, 3, 5]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('viewport filled, scrollback remaining', () => { - beforeEach(() => { - buffer.y = 9; - }); - describe('ybase === 0', () => { - it('should trim the top', () => { - buffer.resize(2, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 3); - assert.equal(buffer.ybase, 3); - assert.equal(buffer.lines.length, 13); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), 'kl'); - for (let i = 6; i < 13; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [1, 3, 5]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('ybase !== 0', () => { - beforeEach(() => { - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should adjust the viewport and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 13); - assert.equal(buffer.ybase, 13); - assert.equal(buffer.lines.length, 23); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'ab'); - assert.equal(buffer.lines.get(11).translateToString(), 'cd'); - assert.equal(buffer.lines.get(12).translateToString(), 'ef'); - assert.equal(buffer.lines.get(13).translateToString(), 'gh'); - assert.equal(buffer.lines.get(14).translateToString(), 'ij'); - assert.equal(buffer.lines.get(15).translateToString(), 'kl'); - for (let i = 16; i < 23; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [11, 13, 15]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should keep ydisp at the same value', () => { - buffer.ydisp = 5; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 13); - assert.equal(buffer.lines.length, 23); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'ab'); - assert.equal(buffer.lines.get(11).translateToString(), 'cd'); - assert.equal(buffer.lines.get(12).translateToString(), 'ef'); - assert.equal(buffer.lines.get(13).translateToString(), 'gh'); - assert.equal(buffer.lines.get(14).translateToString(), 'ij'); - assert.equal(buffer.lines.get(15).translateToString(), 'kl'); - for (let i = 16; i < 23; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [11, 13, 15]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - describe('viewport filled, no scrollback remaining', () => { - // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported - describe('ybase !== 0', () => { - beforeEach(() => { - terminal.options.scrollback = 10; - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should trim lines and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.y = 13; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 10); - assert.equal(buffer.ybase, 10); - assert.equal(buffer.lines.length, 20); - for (let i = 0; i < 7; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(7).translateToString(), 'ab'); - assert.equal(buffer.lines.get(8).translateToString(), 'cd'); - assert.equal(buffer.lines.get(9).translateToString(), 'ef'); - assert.equal(buffer.lines.get(10).translateToString(), 'gh'); - assert.equal(buffer.lines.get(11).translateToString(), 'ij'); - assert.equal(buffer.lines.get(12).translateToString(), 'kl'); - for (let i = 13; i < 20; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [8, 10, 12]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should trim lines and not change ydisp', () => { - buffer.ydisp = 5; - buffer.y = 13; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 10); - assert.equal(buffer.lines.length, 20); - for (let i = 0; i < 7; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(7).translateToString(), 'ab'); - assert.equal(buffer.lines.get(8).translateToString(), 'cd'); - assert.equal(buffer.lines.get(9).translateToString(), 'ef'); - assert.equal(buffer.lines.get(10).translateToString(), 'gh'); - assert.equal(buffer.lines.get(11).translateToString(), 'ij'); - assert.equal(buffer.lines.get(12).translateToString(), 'kl'); - for (let i = 13; i < 20; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [8, 10, 12]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - }); - }); - }); - - describe('buffer marked to have no scrollback', () => { - it('should always have a scrollback of 0', () => { - assert.equal(terminal.options.scrollback, 1000); - // Test size on initialization - buffer = new Buffer(terminal, false); - buffer.fillViewportRows(); - assert.equal(buffer.lines.maxLength, INIT_ROWS); - // Test size on buffer increase - buffer.resize(INIT_COLS, INIT_ROWS * 2); - assert.equal(buffer.lines.maxLength, INIT_ROWS * 2); - // Test size on buffer decrease - buffer.resize(INIT_COLS, INIT_ROWS / 2); - assert.equal(buffer.lines.maxLength, INIT_ROWS / 2); - }); - }); - - describe('addMarker', () => { - it('should adjust a marker line when the buffer is trimmed', () => { - terminal.options.scrollback = 0; - buffer = new Buffer(terminal, true); - buffer.fillViewportRows(); - const marker = buffer.addMarker(buffer.lines.length - 1); - assert.equal(marker.line, buffer.lines.length - 1); - buffer.lines.onTrimEmitter.fire(1); - assert.equal(marker.line, buffer.lines.length - 2); - }); - it('should dispose of a marker if it is trimmed off the buffer', () => { - terminal.options.scrollback = 0; - buffer = new Buffer(terminal, true); - buffer.fillViewportRows(); - assert.equal(buffer.markers.length, 0); - const marker = buffer.addMarker(0); - assert.equal(marker.isDisposed, false); - assert.equal(buffer.markers.length, 1); - buffer.lines.onTrimEmitter.fire(1); - assert.equal(marker.isDisposed, true); - assert.equal(buffer.markers.length, 0); - }); - }); - - describe ('translateBufferLineToString', () => { - it('should handle selecting a section of ascii text', () => { - const line = new BufferLine(4); - line.setCell(0, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - line.setCell(1, CellData.fromCharData([ null, 'b', 1, 'b'.charCodeAt(0)])); - line.setCell(2, CellData.fromCharData([ null, 'c', 1, 'c'.charCodeAt(0)])); - line.setCell(3, CellData.fromCharData([ null, 'd', 1, 'd'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str, 'ab'); - }); - - it('should handle a cut-off double width character by including it', () => { - const line = new BufferLine(3); - line.setCell(0, CellData.fromCharData([ null, '語', 2, 35486 ])); - line.setCell(1, CellData.fromCharData([ null, '', 0, null])); - line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str1, '語'); - }); - - it('should handle a zero width character in the middle of the string by not including it', () => { - const line = new BufferLine(3); - line.setCell(0, CellData.fromCharData([ null, '語', 2, '語'.charCodeAt(0) ])); - line.setCell(1, CellData.fromCharData([ null, '', 0, null])); - line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str0 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str0, '語'); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str1, '語'); - - const str2 = buffer.translateBufferLineToString(0, true, 0, 3); - assert.equal(str2, '語a'); - }); - - it('should handle single width emojis', () => { - const line = new BufferLine(2); - line.setCell(0, CellData.fromCharData([ null, '😁', 1, '😁'.charCodeAt(0) ])); - line.setCell(1, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str1, '😁'); - - const str2 = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str2, '😁a'); - }); - - it('should handle double width emojis', () => { - const line = new BufferLine(2); - line.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); - line.setCell(1, CellData.fromCharData([ null, '', 0, null])); - buffer.lines.set(0, line); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str1, '😁'); - - const str2 = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str2, '😁'); - - const line2 = new BufferLine(3); - line2.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); - line2.setCell(1, CellData.fromCharData([ null, '', 0, null])); - line2.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line2); - - const str3 = buffer.translateBufferLineToString(0, true, 0, 3); - assert.equal(str3, '😁a'); - }); - }); - describe('stringIndexToBufferIndex', () => { - let terminal: TestTerminal; - - beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - }); - - it('multiline ascii', () => { - const input = 'This is ASCII text spanning multiple lines.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - }); - - it('combining e\u0301 in a sentence', () => { - const input = 'Sitting in the cafe\u0301 drinking coffee.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 19; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 18), - terminal.buffer.stringIndexToBufferIndex(0, 19)); - // after the combining char every string index has an offset of -1 - for (let i = 19; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline combining e\u0301', () => { - const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 2 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - it('surrogate char in a sentence', () => { - const input = 'The 𝄞 is a clef widely used in modern notation.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 5; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 4), - terminal.buffer.stringIndexToBufferIndex(0, 5)); - // after the combining char every string index has an offset of -1 - for (let i = 5; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate char', () => { - const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 2 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - it('surrogate char with combining', () => { - // eye of Ra with acute accent - string length of 3 - const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // index 0..2 should map to 0 - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); - for (let i = 2; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate with combining', () => { - const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 3 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth chars', () => { - const input = 'These 123 are some fat numbers.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 6; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 6, 7, 8 take 2 cells - assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); - assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); - // rest of the string has offset of +3 - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); - } - }); - - it('multiline fullwidth chars', () => { - const input = '12345678901234567890'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth combining with emoji - match emoji cell', () => { - const input = 'Lots of ¥\u0301 make me 😃.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - const stringIndex = s.match(/😃/).index; - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); - }); - - it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { - const input = 'a12345678901234567890'; - // the 'a' at the beginning moves all fullwidth chars one to the right - // now the end of the line contains a dangling empty cell since - // the next fullwidth char has to wrap early - // the dangling last cell is wrongly added in the string - // --> fixable after resolving #1685 - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 10; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - const j = (i - 0) << 1; - assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); - } - }); - - it('test fully wrapped buffer up to last char', () => { - const input = Array(6).join('1234567890'); - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - it('test fully wrapped buffer up to last char with full width odd', () => { - const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' - + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal( - (!(i % 3)) - ? input[i] - : (i % 3 === 1) - ? input.substr(i, 2) - : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - it('should handle \t in lines correctly', () => { - const input = '\thttps://google.de'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(s, Array(terminal.options.tabStopWidth + 1).join(' ') + 'https://google.de'); - }); - }); - describe('BufferStringIterator', function(): void { - it('iterator does not overflow buffer limits', function(): void { - const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - const data = [ - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa' - ]; - terminal.writeSync(data.join('')); - // brute force test with insane values - expect(() => { - for (let overscan = 0; overscan < 20; ++overscan) { - for (let start = -10; start < 20; ++start) { - for (let end = -10; end < 20; ++end) { - const it = terminal.buffer.iterator(false, start, end, overscan, overscan); - while (it.hasNext()) { - it.next(); - } - } - } - } - }).to.not.throw(); - }); - }); -}); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 78bebfa5..b4689cd5 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { IInputHandler, IInputHandlingTerminal } from './Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; -import { wcwidth } from './common/CharWidth'; +import { wcwidth } from 'common/CharWidth'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { IDisposable } from 'xterm'; import { Disposable } from 'common/Lifecycle'; diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a66c1388..50794a58 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types'; +import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IMouseZoneManager } from './Types'; +import { IBufferStringIteratorResult } from 'common/buffer/Types'; import { MouseZone } from './MouseZoneManager'; import { getStringCellWidth } from 'common/CharWidth'; import { EventEmitter2, IEvent } from 'common/EventEmitter2'; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 90d70d79..87f7386a 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -6,11 +6,14 @@ import { assert } from 'chai'; import { SelectionManager, SelectionMode } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; -import { BufferSet } from './BufferSet'; -import { ITerminal, IBuffer } from './Types'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { ITerminal } from './Types'; +import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import { MockTerminal, MockCharSizeService } from './TestUtils.test'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; import { BufferLine, CellData } from 'common/buffer/BufferLine'; +import { IBufferService } from 'common/services/Services'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -18,9 +21,10 @@ class TestMockTerminal extends MockTerminal { class TestSelectionManager extends SelectionManager { constructor( - terminal: ITerminal + terminal: ITerminal, + bufferService: IBufferService ) { - super(terminal, new MockCharSizeService(10, 10)); + super(terminal, new MockCharSizeService(10, 10), bufferService); } public get model(): SelectionModel { return this._model; } @@ -40,17 +44,21 @@ class TestSelectionManager extends SelectionManager { describe('SelectionManager', () => { let terminal: ITerminal; let buffer: IBuffer; + let bufferService: IBufferService; let selectionManager: TestSelectionManager; beforeEach(() => { terminal = new TestMockTerminal(); - (terminal as any).cols = 80; - (terminal as any).rows = 2; - terminal.options.scrollback = 100; - terminal.buffers = new BufferSet(terminal); + bufferService = new MockBufferService(20, 20); + terminal.buffers = new BufferSet( + new MockOptionsService({ scrollback: 100 }), + bufferService + ); + terminal.cols = 20; + terminal.rows = 20; terminal.buffer = terminal.buffers.active; buffer = terminal.buffer; - selectionManager = new TestSelectionManager(terminal); + selectionManager = new TestSelectionManager(terminal, bufferService); }); function stringToRow(text: string): IBufferLine { @@ -190,36 +198,36 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'ij"'); }); it('should expand upwards or downards for wrapped lines', () => { - buffer.lines.set(0, stringToRow(' foo')); - buffer.lines.set(1, stringToRow('bar ')); + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('bar ')); buffer.lines.get(1).isWrapped = true; selectionManager.selectWordAt([1, 1]); assert.equal(selectionManager.selectionText, 'foobar'); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([78, 0]); + selectionManager.selectWordAt([18, 0]); assert.equal(selectionManager.selectionText, 'foobar'); }); it('should expand both upwards and downwards for word wrapped over many lines', () => { - const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar'; - buffer.lines.set(0, stringToRow(' foo')); - buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')); - buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')); - buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc')); - buffer.lines.set(4, stringToRow('bar ')); + const expectedText = 'fooaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbccccccccccccccccccccbar'; + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaa')); + buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbb')); + buffer.lines.set(3, stringToRow('cccccccccccccccccccc')); + 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; - selectionManager.selectWordAt([78, 0]); + selectionManager.selectWordAt([18, 0]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([40, 1]); + selectionManager.selectWordAt([10, 1]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([40, 2]); + selectionManager.selectWordAt([10, 2]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([40, 3]); + selectionManager.selectWordAt([10, 3]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); selectionManager.selectWordAt([1, 4]); @@ -340,7 +348,7 @@ describe('SelectionManager', () => { selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct'); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column'); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 0], 'The actual selection spans the entire column'); }); it('should select the entire wrapped line', () => { buffer.lines.set(0, stringToRow('foo')); @@ -350,7 +358,7 @@ describe('SelectionManager', () => { selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct'); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1], 'The actual selection spans the entire column'); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1], 'The actual selection spans the entire column'); }); }); @@ -363,7 +371,7 @@ describe('SelectionManager', () => { buffer.lines.set(3, stringToRow('4')); buffer.lines.set(4, stringToRow('5')); selectionManager.selectAll(); - terminal.buffer.ybase = buffer.lines.length - terminal.rows; + terminal.buffer.ybase = buffer.lines.length - bufferService.rows; assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); }); }); @@ -376,7 +384,7 @@ describe('SelectionManager', () => { buffer.lines.set(2, stringToRow('3')); selectionManager.selectLines(1, 1); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1]); }); it('should select multiple lines', () => { buffer.lines.length = 5; @@ -387,7 +395,7 @@ describe('SelectionManager', () => { buffer.lines.set(4, stringToRow('5')); selectionManager.selectLines(1, 3); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 3]); }); it('should select the to the start when requesting a negative row', () => { buffer.lines.length = 2; @@ -395,7 +403,7 @@ describe('SelectionManager', () => { buffer.lines.set(1, stringToRow('2')); selectionManager.selectLines(-1, 0); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 0]); }); it('should select the to the end when requesting beyond the final row', () => { buffer.lines.length = 2; @@ -403,7 +411,7 @@ describe('SelectionManager', () => { buffer.lines.set(1, stringToRow('2')); selectionManager.selectLines(1, 2); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1]); }); }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 498b4abe..fe36a348 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types'; +import { ITerminal, ISelectionManager, ISelectionRedrawRequestEvent } from './Types'; +import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import { MouseHelper } from './MouseHelper'; import * as Browser from 'common/Platform'; @@ -13,6 +14,7 @@ import { CellData } from 'common/buffer/BufferLine'; import { IDisposable } from 'xterm'; import { EventEmitter2, IEvent } from 'common/EventEmitter2'; import { ICharSizeService } from 'browser/services/Services'; +import { IBufferService } from 'common/services/Services'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -117,12 +119,13 @@ export class SelectionManager implements ISelectionManager { constructor( private _terminal: ITerminal, - private _charSizeService: ICharSizeService + private _charSizeService: ICharSizeService, + bufferService: IBufferService ) { this._initListeners(); this.enable(); - this._model = new SelectionModel(_terminal); + this._model = new SelectionModel(_terminal, bufferService); this._activeSelectionMode = SelectionMode.NORMAL; } diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index c2b261a9..605ba9e8 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -6,14 +6,17 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; import { SelectionModel } from './SelectionModel'; -import { BufferSet } from './BufferSet'; +import { BufferSet } from 'common/buffer/BufferSet'; import { MockTerminal } from './TestUtils.test'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; +import { IBufferService } from 'common/services/Services'; class TestSelectionModel extends SelectionModel { constructor( - terminal: ITerminal + terminal: ITerminal, + bufferService: IBufferService ) { - super(terminal); + super(terminal, bufferService); } } @@ -23,13 +26,14 @@ describe('SelectionManager', () => { beforeEach(() => { terminal = new MockTerminal(); - (terminal as any).cols = 80; - (terminal as any).rows = 2; - terminal.options.scrollback = 10; - terminal.buffers = new BufferSet(terminal); + const bufferService = new MockBufferService(80, 2); + terminal.buffers = new BufferSet( + new MockOptionsService({ scrollback: 10 }), + bufferService + ); terminal.buffer = terminal.buffers.active; - model = new TestSelectionModel(terminal); + model = new TestSelectionModel(terminal, bufferService); }); describe('clearSelection', () => { diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index f87667f2..44cd4cac 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -4,6 +4,7 @@ */ import { ITerminal } from './Types'; +import { IBufferService } from 'common/services/Services'; /** * Represents a selection within the buffer. This model only cares about column @@ -33,7 +34,8 @@ export class SelectionModel { public selectionEnd: [number, number]; constructor( - private _terminal: ITerminal + private _terminal: ITerminal, + private _bufferService: IBufferService ) { this.clearSelection(); } @@ -69,7 +71,7 @@ export class SelectionModel { */ public get finalSelectionEnd(): [number, number] { if (this.isSelectAllActive) { - return [this._terminal.cols, this._terminal.buffer.ybase + this._terminal.rows - 1]; + return [this._bufferService.cols, this._terminal.buffer.ybase + this._bufferService.rows - 1]; } if (!this.selectionStart) { @@ -79,8 +81,8 @@ export class SelectionModel { // Use the selection start + length if the end doesn't exist or they're reversed if (!this.selectionEnd || this.areSelectionValuesReversed()) { const startPlusLength = this.selectionStart[0] + this.selectionStartLength; - if (startPlusLength > this._terminal.cols) { - return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)]; + if (startPlusLength > this._bufferService.cols) { + return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)]; } return [startPlusLength, this.selectionStart[1]]; } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index db474269..a3cff41f 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -379,10 +379,10 @@ describe('Terminal', () => { describe('scrollLines', () => { let startYDisp: number; beforeEach(() => { - for (let i = 0; i < term.rows * 2; i++) { + for (let i = 0; i < INIT_ROWS * 2; i++) { term.writeln('test'); } - startYDisp = term.rows + 1; + startYDisp = INIT_ROWS + 1; }); it('should scroll a single line', () => { assert.equal(term.buffer.ydisp, startYDisp); diff --git a/src/Terminal.ts b/src/Terminal.ts index a5348568..851f4d6e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -23,8 +23,8 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types'; import { IRenderer } from './renderer/Types'; -import { BufferSet } from './BufferSet'; -import { Buffer } from './Buffer'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { Buffer } from 'common/buffer/Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from 'common/EventEmitter'; import { Viewport } from './Viewport'; @@ -51,10 +51,11 @@ import { Attributes, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderCoordinator } from './renderer/RenderCoordinator'; -import { IOptionsService } from 'common/services/Services'; +import { IOptionsService, IBufferService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; +import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -75,9 +76,6 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; const WRITE_TIMEOUT_MS = 12; const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars -const MINIMUM_ROWS = 1; - export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; @@ -108,6 +106,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _customKeyEventHandler: CustomKeyEventHandler; // common services + private _bufferService: IBufferService; public optionsService: IOptionsService; // browser services @@ -188,8 +187,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // bufferline to clone/copy from for new blank lines private _blankLine: IBufferLine = null; - public cols: number; - public rows: number; + public get cols(): number { return this._bufferService.cols; } + public get rows(): number { return this._bufferService.rows; } private _onCursorMove = new EventEmitter2(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } @@ -226,7 +225,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II options: ITerminalOptions = {} ) { super(); + + // Initialize common services this.optionsService = new OptionsService(options); + this._bufferService = new BufferService(this.optionsService); + this._setupOptionsListeners(); // this.options = clone(options); @@ -263,9 +266,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _setup(): void { this._parent = document ? document.body : null; - this.cols = Math.max(this.options.cols, MINIMUM_COLS); - this.rows = Math.max(this.options.rows, MINIMUM_ROWS); - this.cursorState = 0; this.cursorHidden = false; this._customKeyEventHandler = null; @@ -313,7 +313,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.soundManager = this.soundManager || new SoundManager(this); // Create the terminal's buffers and set the current buffer - this.buffers = new BufferSet(this); + this.buffers = new BufferSet(this.optionsService, this._bufferService); if (this.selectionManager) { this.selectionManager.clearSelection(); this.selectionManager.initBuffersListeners(); @@ -643,7 +643,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.register(this.addDisposableListener('focus', () => this._renderCoordinator.onFocus())); this.register(this._renderCoordinator.onDimensionsChange(() => this.viewport.syncScrollArea())); - this.selectionManager = new SelectionManager(this, this._charSizeService); + this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); this.register(this.selectionManager.onRedrawRequest(e => this._renderCoordinator.onSelectionChanged(e.start, e.end, e.columnSelectMode))); @@ -1745,8 +1745,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffers.resize(x, y); - this.cols = x; - this.rows = y; + this._bufferService.resize(x, y); this.buffers.setupTabStops(this.cols); if (this._charSizeService) { diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index e8fdeeba..4697eefb 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -4,9 +4,10 @@ */ import { IRenderer, IRenderDimensions } from './renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler } from './Types'; +import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener } from 'common/Types'; -import { Buffer } from './Buffer'; +import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; @@ -30,8 +31,7 @@ export class MockTerminal implements ITerminal { onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; - onRender: IEvent<{ start: number - ; end: number; }>; + onRender: IEvent<{ start: number; end: number; }>; onResize: IEvent<{ cols: number; rows: number; }>; markers: IMarker[]; optionsService: IOptionsService; diff --git a/src/Types.ts b/src/Types.ts index cb6f4178..9dafbf79 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -4,10 +4,11 @@ */ import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm'; -import { ICharset, IAttributeData, ICellData, IBufferLine, CharData, ICircularList } from 'common/Types'; +import { ICharset, IAttributeData, CharData } from 'common/Types'; import { IEvent } from 'common/EventEmitter2'; import { IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -18,9 +19,6 @@ export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: bo export type CharacterJoinerHandler = (text: string) => [number, number][]; -// BufferIndex denotes a position in the buffer: [rowIndex, colIndex] -export type BufferIndex = [number, number]; - /** * This interface encapsulates everything needed from the Terminal by the * InputHandler. This cleanly separates the large amount of methods needed by @@ -298,52 +296,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions { useFlowControl?: boolean; } -export interface IBufferStringIteratorResult { - range: {first: number, last: number}; - content: string; -} - -export interface IBufferStringIterator { - hasNext(): boolean; - next(): IBufferStringIteratorResult; -} - -export interface IBuffer { - readonly lines: ICircularList; - ydisp: number; - ybase: number; - y: number; - x: number; - tabs: any; - scrollBottom: number; - scrollTop: number; - hasScrollback: boolean; - savedY: number; - savedX: number; - savedCurAttrData: IAttributeData; - isCursorInViewport: boolean; - translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; - getWrappedRangeForLine(y: number): { first: number, last: number }; - nextStop(x?: number): number; - prevStop(x?: number): number; - getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; - stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; - iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; - getNullCell(attr?: IAttributeData): ICellData; - getWhitespaceCell(attr?: IAttributeData): ICellData; -} - -export interface IBufferSet { - alt: IBuffer; - normal: IBuffer; - active: IBuffer; - - onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; - - activateNormalBuffer(): void; - activateAltBuffer(fillAttr?: IAttributeData): void; -} - export interface ISelectionManager { selectionText: string; selectionStart: [number, number]; diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 60ce685e..9312e383 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -72,7 +72,7 @@ class DomMeasureStrategy implements IMeasureStrategy { // Note that this triggers a synchronous layout const geometry = this._measureElement.getBoundingClientRect(); -console.log('measure', geometry); + // If values are 0 then the element is likely currently display:none, in which case we should // retain the previous value. if (geometry.width !== 0 && geometry.height !== 0) { diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts new file mode 100644 index 00000000..0a0cb6a4 --- /dev/null +++ b/src/common/TestUtils.test.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; +import { IEvent, EventEmitter2 } from 'common/EventEmitter2'; +import { clone } from 'common/Clone'; +import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; + +export class MockBufferService implements IBufferService { + constructor( + public cols: number, + public rows: number + ) {} + resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + } +} + +export class MockOptionsService implements IOptionsService { + options: ITerminalOptions = clone(DEFAULT_OPTIONS); + onOptionChange: IEvent = new EventEmitter2().event; + constructor(testOptions: IPartialTerminalOptions) { + Object.keys(testOptions).forEach(key => this.options[key] = (testOptions)[key]); + } + setOption(key: string, value: T): void { + throw new Error('Method not implemented.'); + } + getOption(key: string): T { + throw new Error('Method not implemented.'); + } +} diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts new file mode 100644 index 00000000..01516b3c --- /dev/null +++ b/src/common/buffer/Buffer.test.ts @@ -0,0 +1,1398 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { Buffer } from 'common/buffer/Buffer'; +import { CircularList } from 'common/CircularList'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; +import { BufferLine, CellData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; + +const INIT_COLS = 80; +const INIT_ROWS = 24; +const INIT_SCROLLBACK = 1000; + +describe('Buffer', () => { + let optionsService: MockOptionsService; + let bufferService: MockBufferService; + let buffer: Buffer; + + beforeEach(() => { + optionsService = new MockOptionsService({ scrollback: INIT_SCROLLBACK }); + bufferService = new MockBufferService(INIT_COLS, INIT_ROWS); + buffer = new Buffer(true, optionsService, bufferService); + }); + + describe('constructor', () => { + it('should create a CircularList with max length equal to rows + scrollback, for its lines', () => { + assert.instanceOf(buffer.lines, CircularList); + assert.equal(buffer.lines.maxLength, bufferService.rows + INIT_SCROLLBACK); + }); + it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => { + assert.equal(buffer.scrollBottom, bufferService.rows - 1); + }); + }); + + describe('fillViewportRows', () => { + it('should fill the buffer with blank lines based on the size of the viewport', () => { + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData(); + 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)!.loadCell(x, new CellData()).getAsCharData(), blankLineChar); + } + } + }); + }); + + describe('getWrappedRangeForLine', () => { + describe('non-wrapped', () => { + it('should return a single row for the first row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 }); + }); + it('should return a single row for a middle row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 }); + }); + it('should return a single row for the last row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 }); + }); + }); + describe('wrapped', () => { + it('should return a range for the first row', () => { + buffer.fillViewportRows(); + 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; + 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; + 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; + 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; + 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; + 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; + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); + }); + }); + }); + + describe('resize', () => { + describe('column size is reduced', () => { + it('should trim the data in the buffer', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS / 2, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i)!.length, INIT_COLS / 2); + } + }); + }); + + describe('column size is increased', () => { + it('should add pad columns', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS + 10, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i)!.length, INIT_COLS + 10); + } + }); + }); + + describe('row size reduced', () => { + it('should trim blank lines from the end', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS, INIT_ROWS - 10); + assert.equal(buffer.lines.length, INIT_ROWS - 10); + }); + + it('should move the viewport down when it\'s at the end', () => { + buffer.fillViewportRows(); + // Set cursor y to have 5 blank lines below it + buffer.y = INIT_ROWS - 5 - 1; + buffer.resize(INIT_COLS, INIT_ROWS - 10); + // Trim 5 rows + assert.equal(buffer.lines.length, INIT_ROWS - 5); + // Shift the viewport down 5 rows + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 5); + }); + + describe('no scrollback', () => { + it('should trim from the top of the buffer when the cursor reaches the bottom', () => { + buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService); + assert.equal(buffer.lines.maxLength, INIT_ROWS); + buffer.y = INIT_ROWS - 1; + buffer.fillViewportRows(); + let chData = buffer.lines.get(5)!.loadCell(0, new CellData()).getAsCharData(); + chData[1] = 'a'; + buffer.lines.get(5)!.setCell(0, CellData.fromCharData(chData)); + chData = buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getAsCharData(); + chData[1] = 'b'; + buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData(chData)); + buffer.resize(INIT_COLS, INIT_ROWS - 5); + assert.equal(buffer.lines.get(0)!.loadCell(0, new CellData()).getAsCharData()[1], 'a'); + assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)!.loadCell(0, new CellData()).getAsCharData()[1], 'b'); + }); + }); + }); + + describe('row size increased', () => { + describe('empty buffer', () => { + it('should add blank lines to end', () => { + buffer.fillViewportRows(); + assert.equal(buffer.ydisp, 0); + buffer.resize(INIT_COLS, INIT_ROWS + 10); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + }); + + describe('filled buffer', () => { + it('should show more of the buffer above', () => { + buffer.fillViewportRows(); + // Create 10 extra blank lines + for (let i = 0; i < 10; i++) { + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + // Set cursor to the bottom of the buffer + buffer.y = INIT_ROWS - 1; + // Scroll down 10 lines + buffer.ybase = 10; + buffer.ydisp = 10; + assert.equal(buffer.lines.length, INIT_ROWS + 10); + buffer.resize(INIT_COLS, INIT_ROWS + 5); + // Should be should 5 more lines + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 5); + // Should not trim the buffer + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + + it('should show more of the buffer below when the viewport is at the top of the buffer', () => { + buffer.fillViewportRows(); + // Create 10 extra blank lines + for (let i = 0; i < 10; i++) { + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + // Set cursor to the bottom of the buffer + buffer.y = INIT_ROWS - 1; + // Scroll down 10 lines + buffer.ybase = 10; + buffer.ydisp = 0; + assert.equal(buffer.lines.length, INIT_ROWS + 10); + buffer.resize(INIT_COLS, INIT_ROWS + 5); + // The viewport should remain at the top + assert.equal(buffer.ydisp, 0); + // The buffer ybase should move up 5 lines + assert.equal(buffer.ybase, 5); + // Should not trim the buffer + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + }); + }); + + describe('row and column increased', () => { + it('should resize properly', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS + 5, INIT_ROWS + 5); + assert.equal(buffer.lines.length, INIT_ROWS + 5); + for (let i = 0; i < INIT_ROWS + 5; i++) { + assert.equal(buffer.lines.get(i)!.length, INIT_COLS + 5); + } + }); + }); + + describe('reflow', () => { + it('should not wrap empty lines', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + buffer.resize(INIT_COLS - 5, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + }); + it('should shrink row length', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.length, 5); + assert.equal(buffer.lines.get(1)!.length, 5); + assert.equal(buffer.lines.get(2)!.length, 5); + assert.equal(buffer.lines.get(3)!.length, 5); + assert.equal(buffer.lines.get(4)!.length, 5); + assert.equal(buffer.lines.get(5)!.length, 5); + assert.equal(buffer.lines.get(6)!.length, 5); + assert.equal(buffer.lines.get(7)!.length, 5); + assert.equal(buffer.lines.get(8)!.length, 5); + assert.equal(buffer.lines.get(9)!.length, 5); + }); + it('should wrap and unwrap lines', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + const firstLine = buffer.lines.get(0)!; + for (let i = 0; i < 5; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [0, char, 1, code]); + } + buffer.y = 1; + assert.equal(buffer.lines.get(0)!.length, 5); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcde'); + buffer.resize(1, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'a'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'b'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'c'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'd'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'e'); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + assert.equal(buffer.lines.get(6)!.translateToString(), ' '); + assert.equal(buffer.lines.get(7)!.translateToString(), ' '); + assert.equal(buffer.lines.get(8)!.translateToString(), ' '); + assert.equal(buffer.lines.get(9)!.translateToString(), ' '); + buffer.resize(5, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcde'); + assert.equal(buffer.lines.get(1)!.translateToString(), ' '); + assert.equal(buffer.lines.get(2)!.translateToString(), ' '); + assert.equal(buffer.lines.get(3)!.translateToString(), ' '); + assert.equal(buffer.lines.get(4)!.translateToString(), ' '); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + assert.equal(buffer.lines.get(6)!.translateToString(), ' '); + assert.equal(buffer.lines.get(7)!.translateToString(), ' '); + assert.equal(buffer.lines.get(8)!.translateToString(), ' '); + assert.equal(buffer.lines.get(9)!.translateToString(), ' '); + }); + it('should discard parts of wrapped lines that go out of the scrollback', () => { + buffer.fillViewportRows(); + optionsService.options.scrollback = 1; + buffer.resize(10, 5); + const lastLine = buffer.lines.get(3)!; + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + lastLine.set(i, [0, char, 1, code]); + } + assert.equal(buffer.lines.length, 5); + buffer.y = 4; + buffer.resize(2, 5); + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + buffer.resize(1, 5); + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0)!.translateToString(), 'f'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'g'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'h'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'i'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'j'); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + buffer.resize(10, 5); + assert.equal(buffer.y, 1); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 5); + assert.equal(buffer.lines.get(0)!.translateToString(), 'fghij '); + assert.equal(buffer.lines.get(1)!.translateToString(), ' '); + assert.equal(buffer.lines.get(2)!.translateToString(), ' '); + assert.equal(buffer.lines.get(3)!.translateToString(), ' '); + assert.equal(buffer.lines.get(4)!.translateToString(), ' '); + }); + it('should remove the correct amount of rows when reflowing larger', () => { + // This is a regression test to ensure that successive wrapped lines that are getting + // 3+ lines removed on a reflow actually remove the right lines + buffer.fillViewportRows(); + buffer.resize(10, 10); + buffer.y = 2; + const firstLine = buffer.lines.get(0)!; + const secondLine = buffer.lines.get(1)!; + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + secondLine.set(i, [0, char, 1, code]); + } + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + buffer.resize(2, 10); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 11); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), '01'); + assert.equal(buffer.lines.get(6)!.translateToString(), '23'); + assert.equal(buffer.lines.get(7)!.translateToString(), '45'); + assert.equal(buffer.lines.get(8)!.translateToString(), '67'); + assert.equal(buffer.lines.get(9)!.translateToString(), '89'); + assert.equal(buffer.lines.get(10)!.translateToString(), ' '); + buffer.resize(10, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + }); + it('should transfer combined char data over to reflowed lines', () => { + buffer.fillViewportRows(); + buffer.resize(4, 3); + buffer.y = 2; + const firstLine = buffer.lines.get(0)!; + firstLine.set(0, [ 0, 'a', 1, 'a'.charCodeAt(0) ]); + firstLine.set(1, [ 0, 'b', 1, 'b'.charCodeAt(0) ]); + firstLine.set(2, [ 0, 'c', 1, 'c'.charCodeAt(0) ]); + firstLine.set(3, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); + assert.equal(buffer.lines.length, 3); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abc😁'); + assert.equal(buffer.lines.get(1)!.translateToString(), ' '); + buffer.resize(2, 3); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'c😁'); + }); + it('should adjust markers when reflowing', () => { + buffer.fillViewportRows(); + buffer.resize(10, 16); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2)!.set(i, [0, char, 1, code]); + } + buffer.y = 3; + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 16); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), '01'); + assert.equal(buffer.lines.get(6)!.translateToString(), '23'); + assert.equal(buffer.lines.get(7)!.translateToString(), '45'); + assert.equal(buffer.lines.get(8)!.translateToString(), '67'); + assert.equal(buffer.lines.get(9)!.translateToString(), '89'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'kl'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'mn'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'op'); + assert.equal(buffer.lines.get(13)!.translateToString(), 'qr'); + assert.equal(buffer.lines.get(14)!.translateToString(), 'st'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); + assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); + buffer.resize(10, 16); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); + assert.equal(firstMarker.isDisposed, false); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + }); + it('should dispose markers whose rows are trimmed during a reflow', () => { + buffer.fillViewportRows(); + optionsService.options.scrollback = 1; + buffer.resize(10, 11); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2)!.set(i, [0, char, 1, code]); + } + buffer.y = 10; + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + buffer.y = 3; + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 11); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '01'); + assert.equal(buffer.lines.get(2)!.translateToString(), '23'); + assert.equal(buffer.lines.get(3)!.translateToString(), '45'); + assert.equal(buffer.lines.get(4)!.translateToString(), '67'); + assert.equal(buffer.lines.get(5)!.translateToString(), '89'); + assert.equal(buffer.lines.get(6)!.translateToString(), 'kl'); + assert.equal(buffer.lines.get(7)!.translateToString(), 'mn'); + assert.equal(buffer.lines.get(8)!.translateToString(), 'op'); + assert.equal(buffer.lines.get(9)!.translateToString(), 'qr'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'st'); + assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4'); + assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped'); + assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + buffer.resize(10, 11); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ij '); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(secondMarker.line, 1, 'second marker should be restored'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored'); + }); + it('should correctly reflow wrapped lines that end in 0 space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(5, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), 'ab c'); + assert.equal(buffer.lines.get(1)!.translateToString(false), 'd '); + buffer.resize(6, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), 'ab cd'); + assert.equal(buffer.lines.get(1)!.translateToString(false), ' '); + }); + it('should wrap wide characters correctly when reflowing larger', () => { + buffer.fillViewportRows(); + buffer.resize(12, 10); + buffer.y = 2; + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0)!.set(i, [0, '', 0, 0]); + buffer.lines.get(1)!.set(i, [0, '', 0, 0]); + } + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语汉语'); + buffer.resize(13, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(0)!.translateToString(false), '汉语汉语汉语 '); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(false), '汉语汉语汉语 '); + buffer.resize(14, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(0)!.translateToString(false), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(false), '语汉语汉语 '); + }); + it('should correctly reflow wrapped lines that end in 0 space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(3, 10); + assert.equal(buffer.y, 2); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(false), 'ab '); + assert.equal(buffer.lines.get(1)!.translateToString(false), ' cd'); + buffer.resize(2, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(false), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(false), ' '); + assert.equal(buffer.lines.get(2)!.translateToString(false), 'cd'); + }); + it('should wrap wide characters correctly when reflowing smaller', () => { + buffer.fillViewportRows(); + buffer.resize(12, 10); + buffer.y = 2; + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0)!.set(i, [0, '', 0, 0]); + buffer.lines.get(1)!.set(i, [0, '', 0, 0]); + } + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语汉语'); + buffer.resize(11, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语'); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语'); + buffer.resize(9, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉语'); + buffer.resize(8, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉语'); + buffer.resize(7, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3)!.translateToString(true), '语汉语'); + buffer.resize(6, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3)!.translateToString(true), '语汉语'); + }); + + describe('reflowLarger cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'ab' + // 'cd' (wrapped) + // 'ef' + // 'gh' (wrapped) + // 'ij' + // 'kl' (wrapped) + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(2, 10); + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.isWrapped = true; + buffer.lines.get(2)!.set(0, [0, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(2)!.set(1, [0, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(3)!.set(0, [0, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(3)!.set(1, [0, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(3)!.isWrapped = true; + buffer.lines.get(4)!.set(0, [0, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(4)!.set(1, [0, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(5)!.set(0, [0, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(5)!.set(1, [0, 'l', 1, 'l'.charCodeAt(0)]); + buffer.lines.get(5)!.isWrapped = true; + }); + describe('viewport not yet filled', () => { + it('should move the cursor up and add empty lines', () => { + buffer.y = 6; + buffer.resize(4, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should move the cursor up and add empty lines', () => { + buffer.resize(4, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + optionsService.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.y = 9; + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); + describe('reflowSmaller cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'abcd' + // 'efgh' (wrapped) + // 'ijkl' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(0)!.set(2, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(0)!.set(3, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(1)!.set(2, [0, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(1)!.set(3, [0, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(2)!.set(0, [0, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(2)!.set(1, [0, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(2)!.set(2, [0, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(2)!.set(3, [0, 'l', 1, 'l'.charCodeAt(0)]); + }); + describe('viewport not yet filled', () => { + it('should move the cursor down', () => { + buffer.y = 3; + buffer.resize(2, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), 'kl'); + for (let i = 6; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should trim the top', () => { + buffer.resize(2, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 3); + assert.equal(buffer.ybase, 3); + assert.equal(buffer.lines.length, 13); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), 'kl'); + for (let i = 6; i < 13; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 13); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(13)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(14)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(15)!.translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(13)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(14)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(15)!.translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + optionsService.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.y = 13; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 10); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(7)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(8)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(9)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.y = 13; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(7)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(8)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(9)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); + }); + }); + + describe('buffer marked to have no scrollback', () => { + it('should always have a scrollback of 0', () => { + // Test size on initialization + buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService); + buffer.fillViewportRows(); + assert.equal(buffer.lines.maxLength, INIT_ROWS); + // Test size on buffer increase + buffer.resize(INIT_COLS, INIT_ROWS * 2); + assert.equal(buffer.lines.maxLength, INIT_ROWS * 2); + // Test size on buffer decrease + buffer.resize(INIT_COLS, INIT_ROWS / 2); + assert.equal(buffer.lines.maxLength, INIT_ROWS / 2); + }); + }); + + describe('addMarker', () => { + it('should adjust a marker line when the buffer is trimmed', () => { + buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService); + buffer.fillViewportRows(); + const marker = buffer.addMarker(buffer.lines.length - 1); + assert.equal(marker.line, buffer.lines.length - 1); + buffer.lines.onTrimEmitter.fire(1); + assert.equal(marker.line, buffer.lines.length - 2); + }); + it('should dispose of a marker if it is trimmed off the buffer', () => { + buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService); + buffer.fillViewportRows(); + assert.equal(buffer.markers.length, 0); + const marker = buffer.addMarker(0); + assert.equal(marker.isDisposed, false); + assert.equal(buffer.markers.length, 1); + buffer.lines.onTrimEmitter.fire(1); + assert.equal(marker.isDisposed, true); + assert.equal(buffer.markers.length, 0); + }); + }); + + describe ('translateBufferLineToString', () => { + it('should handle selecting a section of ascii text', () => { + const line = new BufferLine(4); + line.setCell(0, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([ 0, 'b', 1, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([ 0, 'c', 1, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([ 0, 'd', 1, 'd'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str, 'ab'); + }); + + it('should handle a cut-off double width character by including it', () => { + const line = new BufferLine(3); + line.setCell(0, CellData.fromCharData([ 0, '語', 2, 35486 ])); + line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str1, '語'); + }); + + it('should handle a zero width character in the middle of the string by not including it', () => { + const line = new BufferLine(3); + line.setCell(0, CellData.fromCharData([ 0, '語', 2, '語'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str0 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str0, '語'); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str1, '語'); + + const str2 = buffer.translateBufferLineToString(0, true, 0, 3); + assert.equal(str2, '語a'); + }); + + it('should handle single width emojis', () => { + const line = new BufferLine(2); + line.setCell(0, CellData.fromCharData([ 0, '😁', 1, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str1, '😁'); + + const str2 = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str2, '😁a'); + }); + + it('should handle double width emojis', () => { + const line = new BufferLine(2); + line.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + buffer.lines.set(0, line); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str1, '😁'); + + const str2 = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str2, '😁'); + + const line2 = new BufferLine(3); + line2.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ])); + line2.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + line2.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line2); + + const str3 = buffer.translateBufferLineToString(0, true, 0, 3); + assert.equal(str3, '😁a'); + }); + }); + // describe('stringIndexToBufferIndex', () => { + // let terminal: TestTerminal; + + // beforeEach(() => { + // terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + // }); + + // it('multiline ascii', () => { + // const input = 'This is ASCII text spanning multiple lines.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // }); + + // it('combining e\u0301 in a sentence', () => { + // const input = 'Sitting in the cafe\u0301 drinking coffee.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < 19; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index + // assert.deepEqual( + // terminal.buffer.stringIndexToBufferIndex(0, 18), + // terminal.buffer.stringIndexToBufferIndex(0, 19)); + // // after the combining char every string index has an offset of -1 + // for (let i = 19; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline combining e\u0301', () => { + // const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // every buffer cell index contains 2 string indices + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('surrogate char in a sentence', () => { + // const input = 'The 𝄞 is a clef widely used in modern notation.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < 5; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index + // assert.deepEqual( + // terminal.buffer.stringIndexToBufferIndex(0, 4), + // terminal.buffer.stringIndexToBufferIndex(0, 5)); + // // after the combining char every string index has an offset of -1 + // for (let i = 5; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline surrogate char', () => { + // const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // every buffer cell index contains 2 string indices + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('surrogate char with combining', () => { + // // eye of Ra with acute accent - string length of 3 + // const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // index 0..2 should map to 0 + // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); + // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); + // for (let i = 2; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline surrogate with combining', () => { + // const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // every buffer cell index contains 3 string indices + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); + // } + // }); + + // it('fullwidth chars', () => { + // const input = 'These 123 are some fat numbers.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < 6; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // // string index 6, 7, 8 take 2 cells + // assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); + // assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); + // // rest of the string has offset of +3 + // for (let i = 9; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline fullwidth chars', () => { + // const input = '12345678901234567890'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 9; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('fullwidth combining with emoji - match emoji cell', () => { + // const input = 'Lots of ¥\u0301 make me 😃.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // const stringIndex = s.match(/😃/).index; + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); + // assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); + // }); + + // it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { + // const input = 'a12345678901234567890'; + // // the 'a' at the beginning moves all fullwidth chars one to the right + // // now the end of the line contains a dangling empty cell since + // // the next fullwidth char has to wrap early + // // the dangling last cell is wrongly added in the string + // // --> fixable after resolving #1685 + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 10; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + // const j = (i - 0) << 1; + // assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); + // } + // }); + + // it('test fully wrapped buffer up to last char', () => { + // const input = Array(6).join('1234567890'); + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + // assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + // } + // }); + + // it('test fully wrapped buffer up to last char with full width odd', () => { + // const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' + // + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + // assert.equal( + // (!(i % 3)) + // ? input[i] + // : (i % 3 === 1) + // ? input.substr(i, 2) + // : input.substr(i - 1, 2), + // terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + // } + // }); + + // it('should handle \t in lines correctly', () => { + // const input = '\thttps://google.de'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(s, Array(optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); + // }); + // }); + // describe('BufferStringIterator', function(): void { + // it('iterator does not overflow buffer limits', function(): void { + // const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + // const data = [ + // 'aaaaaaaaaa', + // 'aaaaaaaaa\n', + // 'aaaaaaaaaa', + // 'aaaaaaaaa\n', + // 'aaaaaaaaaa', + // 'aaaaaaaaaa', + // 'aaaaaaaaaa', + // 'aaaaaaaaa\n', + // 'aaaaaaaaaa', + // 'aaaaaaaaaa' + // ]; + // terminal.writeSync(data.join('')); + // // brute force test with insane values + // expect(() => { + // for (let overscan = 0; overscan < 20; ++overscan) { + // for (let start = -10; start < 20; ++start) { + // for (let end = -10; end < 20; ++end) { + // const it = terminal.buffer.iterator(false, start, end, overscan, overscan); + // while (it.hasNext()) { + // it.next(); + // } + // } + // } + // } + // }).to.not.throw(); + // }); + // }); +}); diff --git a/src/Buffer.ts b/src/common/buffer/Buffer.ts similarity index 94% rename from src/Buffer.ts rename to src/common/buffer/Buffer.ts index 6b89884e..1e6edae9 100644 --- a/src/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -4,11 +4,12 @@ */ import { CircularList, IInsertEvent } from 'common/CircularList'; -import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; +import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData } from 'common/Types'; import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'common/buffer/BufferReflow'; import { Marker } from 'common/buffer/Marker'; +import { IOptionsService, IBufferService } from 'common/services/Services'; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 @@ -21,15 +22,16 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 */ export class Buffer implements IBuffer { public lines: CircularList; - public ydisp: number; - public ybase: number; - public y: number; - public x: number; + public ydisp: number = 0; + public ybase: number = 0; + public y: number = 0; + public x: number = 0; public scrollBottom: number; public scrollTop: number; + // TODO: Type me public tabs: any; - public savedY: number; - public savedX: number; + public savedY: number = 0; + public savedX: number = 0; public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); public markers: Marker[] = []; private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); @@ -37,19 +39,17 @@ export class Buffer implements IBuffer { private _cols: number; private _rows: number; - /** - * Create a new Buffer. - * @param _terminal The terminal the Buffer will belong to. - * @param _hasScrollback Whether the buffer should respect the scrollback of - * the terminal. - */ constructor( - private _terminal: ITerminal, - private _hasScrollback: boolean + private _hasScrollback: boolean, + private _optionsService: IOptionsService, + private _bufferService: IBufferService ) { - this._cols = this._terminal.cols; - this._rows = this._terminal.rows; - this.clear(); + this._cols = this._bufferService.cols; + this._rows = this._bufferService.rows; + this.lines = new CircularList(this._getCorrectBufferLength(this._rows)); + this.scrollTop = 0; + this.scrollBottom = this._rows - 1; + this.setupTabStops(); } public getNullCell(attr?: IAttributeData): ICellData { @@ -75,7 +75,7 @@ export class Buffer implements IBuffer { } public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { - return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped); + return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped); } public get hasScrollback(): boolean { @@ -98,7 +98,7 @@ export class Buffer implements IBuffer { return rows; } - const correctBufferLength = rows + this._terminal.options.scrollback; + const correctBufferLength = rows + this._optionsService.options.scrollback; return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength; } @@ -154,7 +154,7 @@ export class Buffer implements IBuffer { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, nullCell); + this.lines.get(i)!.resize(newCols, nullCell); } } @@ -227,7 +227,7 @@ export class Buffer implements IBuffer { // Trim the end of the line off if cols shrunk if (this._cols > newCols) { for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, nullCell); + this.lines.get(i)!.resize(newCols, nullCell); } } } @@ -237,7 +237,7 @@ export class Buffer implements IBuffer { } private get _isReflowEnabled(): boolean { - return this._hasScrollback && !this._terminal.options.windowsMode; + return this._hasScrollback && !this._optionsService.options.windowsMode; } private _reflow(newCols: number, newRows: number): void { @@ -509,11 +509,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 }; @@ -533,7 +533,7 @@ export class Buffer implements IBuffer { i = 0; } - for (; i < this._cols; i += this._terminal.options.tabStopWidth) { + for (; i < this._cols; i += this._optionsService.options.tabStopWidth) { this.tabs[i] = true; } } diff --git a/src/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts similarity index 83% rename from src/BufferSet.test.ts rename to src/common/buffer/BufferSet.test.ts index cdc220b3..894ebb20 100644 --- a/src/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -4,21 +4,18 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Types'; -import { BufferSet } from './BufferSet'; -import { Buffer } from './Buffer'; -import { MockTerminal } from './TestUtils.test'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { Buffer } from 'common/buffer/Buffer'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; describe('BufferSet', () => { - let terminal: ITerminal; let bufferSet: BufferSet; beforeEach(() => { - terminal = new MockTerminal(); - (terminal as any).cols = 80; - (terminal as any).rows = 24; - terminal.options.scrollback = 1000; - bufferSet = new BufferSet(terminal); + bufferSet = new BufferSet( + new MockOptionsService({ scrollback: 1000 }), + new MockBufferService(80, 24) + ); }); describe('constructor', () => { diff --git a/src/BufferSet.ts b/src/common/buffer/BufferSet.ts similarity index 88% rename from src/BufferSet.ts rename to src/common/buffer/BufferSet.ts index 1d4ae2c6..cc8fe737 100644 --- a/src/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { ITerminal, IBufferSet, IBuffer } from './Types'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IAttributeData } from 'common/Types'; -import { Buffer } from './Buffer'; +import { Buffer } from 'common/buffer/Buffer'; import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { IOptionsService, IBufferService } from 'common/services/Services'; /** * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and @@ -25,13 +26,16 @@ export class BufferSet implements IBufferSet { * Create a new BufferSet for the given terminal. * @param _terminal - The terminal the BufferSet will belong to */ - constructor(private _terminal: ITerminal) { - this._normal = new Buffer(this._terminal, true); + constructor( + readonly optionsService: IOptionsService, + readonly bufferService: IBufferService + ) { + this._normal = new Buffer(true, optionsService, bufferService); this._normal.fillViewportRows(); // The alt buffer should never have scrollback. // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer - this._alt = new Buffer(this._terminal, false); + this._alt = new Buffer(false, optionsService, bufferService); this._activeBuffer = this._normal; this.setupTabStops(); diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts new file mode 100644 index 00000000..37ce2b7e --- /dev/null +++ b/src/common/buffer/Types.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IAttributeData, ICircularList, IBufferLine, ICellData } from 'common/Types'; +import { IEvent } from 'common/EventEmitter2'; + +// BufferIndex denotes a position in the buffer: [rowIndex, colIndex] +export type BufferIndex = [number, number]; + +export interface IBufferStringIteratorResult { + range: {first: number, last: number}; + content: string; +} + +export interface IBufferStringIterator { + hasNext(): boolean; + next(): IBufferStringIteratorResult; +} + +export interface IBuffer { + readonly lines: ICircularList; + ydisp: number; + ybase: number; + y: number; + x: number; + tabs: any; + scrollBottom: number; + scrollTop: number; + hasScrollback: boolean; + savedY: number; + savedX: number; + savedCurAttrData: IAttributeData; + isCursorInViewport: boolean; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + getWrappedRangeForLine(y: number): { first: number, last: number }; + nextStop(x?: number): number; + prevStop(x?: number): number; + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; + getNullCell(attr?: IAttributeData): ICellData; + getWhitespaceCell(attr?: IAttributeData): ICellData; +} + +export interface IBufferSet { + alt: IBuffer; + normal: IBuffer; + active: IBuffer; + + onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; + + activateNormalBuffer(): void; + activateAltBuffer(fillAttr?: IAttributeData): void; +} diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts new file mode 100644 index 00000000..542166c1 --- /dev/null +++ b/src/common/services/BufferService.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IOptionsService } from './Services'; + +export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars +export const MINIMUM_ROWS = 1; + +export class BufferService implements IBufferService { + public cols: number; + public rows: number; + + constructor( + optionsService: IOptionsService + ) { + this.cols = Math.max(optionsService.options.cols, MINIMUM_COLS); + this.rows = Math.max(optionsService.options.rows, MINIMUM_ROWS); + } + + public resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + } +} diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 0fce405d..d518aae6 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -15,7 +15,7 @@ import { clone } from 'common/Clone'; export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; // TODO: Freeze? -const DEFAULT_OPTIONS: ITerminalOptions = { +export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ cols: 80, rows: 24, cursorBlink: false, @@ -47,7 +47,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { debug: false, cancelEvents: false, useFlowControl: false -}; +}); /** * The set of options that only have an effect when set in the Terminal constructor. diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.d.ts index 651e8636..8ebc0308 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.d.ts @@ -5,6 +5,15 @@ import { IEvent } from 'common/EventEmitter2'; +export interface IBufferService { + readonly cols: number; + readonly rows: number; + + // TODO: Move resize event here + + resize(cols: number, rows: number): void; +} + export interface IOptionsService { readonly options: ITerminalOptions; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index f2524381..5c7b4b99 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,8 +4,9 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; -import { ITerminal, IBuffer } from '../Types'; +import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; +import { IBuffer } from 'common/buffer/Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; import { IEvent } from 'common/EventEmitter2'; diff --git a/tslint.json b/tslint.json index d2daef09..f6f1f46c 100644 --- a/tslint.json +++ b/tslint.json @@ -95,7 +95,7 @@ {"type": "default", "format": "camelCase", "leadingUnderscore": "forbid"}, {"type": "type", "format": "PascalCase"}, {"type": "class", "format": "PascalCase"}, - {"type": "property", "modifiers": ["const"], "format": "UPPER_CASE"}, + {"type": "property", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]}, {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "allow"}, // TODO: Change allow to require when there aren't many PRs out // {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"},