diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 714257f9..e0f9f878 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -1123,6 +1123,241 @@ describe('Terminal', () => { }); }); }); + + describe('Buffer.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.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(); + }); + }); }); class TestLinkifier extends Linkifier { diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index af5abeca..35aade4a 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -1163,237 +1163,4 @@ describe('Buffer', () => { 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/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 532230ec..e229d69c 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -40,7 +40,7 @@ export interface IBuffer { nextStop(x?: number): number; prevStop(x?: number): number; getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; - stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; getNullCell(attr?: IAttributeData): ICellData; getWhitespaceCell(attr?: IAttributeData): ICellData;