diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 6f417f8e..5a43a5fe 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,10 +5,11 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; -import { Buffer, DEFAULT_ATTR } from './Buffer'; +import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; import { BufferLine } from './BufferLine'; +import { Terminal } from './Terminal'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -347,4 +348,153 @@ describe('Buffer', () => { assert.equal(str3, '😁a'); }); }); + describe('stringIndexToBufferIndex', function(): void { + beforeEach(function(): void { + terminal = new Terminal({rows: 5, cols: 10}); + const oldWrite: any = terminal.write.bind(terminal); + terminal.write = (s: string): void => { + oldWrite(s); + (terminal as any)._innerWrite(); + } + }); + function wholeString(terminal: ITerminal, range : {first: number, last: number}): string { + let result = ''; + for (let i = range.first; i <= range.last; ++i) { + result += terminal.buffer.translateBufferLineToString(i, i == range.last); + } + return result; + } + it('multiline ascii', function(): void { + const input = 'This is ASCII text spanning multiple lines.'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = 'Sitting in the cafe\u0301 drinking coffee.'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = 'The 𝄞 is a clef widely used in modern notation.'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + // eye of Ra with acute accent - string length of 3 + const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = 'These 123 are some fat numbers.'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = '12345678901234567890'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + 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', function(): void { + const input = 'Lots of ¥\u0301 make me 😃.'; + terminal.write(input); + const s = wholeString(terminal, terminal.buffer.getWrappedRangeForLine(0)); + assert.equal(input, s); + const stringIndex = s.match(/😃/).index; + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); + assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃'); + }); + }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 9487be4a..cb7d3b87 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -194,6 +194,26 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; } + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { + if (!stringIndex) { + return [lineIndex, 0]; + } + while (stringIndex) { + let line = this.lines.get(lineIndex); + if (!line) { + [-1, -1]; + } + for (let i = 0; i < line.length; ++i) { + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; + if (stringIndex < 0) { + return [lineIndex, i]; + } + } + lineIndex++; + } + return [lineIndex, 0]; + } + /** * Translates a buffer line to a string, with optional start and end columns. * Wide characters will count as two columns in the resulting string. This diff --git a/src/Types.ts b/src/Types.ts index df3f1a79..820a8b89 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -282,6 +282,7 @@ export interface IBuffer { getWrappedRangeForLine(y: number): { first: number, last: number }; nextStop(x?: number): number; prevStop(x?: number): number; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; } export interface IBufferSet extends IEventEmitter {