diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 5a43a5fe..1bd1ea38 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -355,33 +355,26 @@ describe('Buffer', () => { 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)); + const s = terminal.buffer.contents(true).toArray()[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); + 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)); + const s = terminal.buffer.contents(true).toArray()[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); + 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( @@ -390,28 +383,28 @@ describe('Buffer', () => { // 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); + 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)); + const s = terminal.buffer.contents(true).toArray()[0]; assert.equal(input, s); - // every buffer cell index contains 2 string indices + // 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); + 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)); + const s = terminal.buffer.contents(true).toArray()[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); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); } // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index assert.deepEqual( @@ -420,53 +413,53 @@ describe('Buffer', () => { // 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); + 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)); + const s = terminal.buffer.contents(true).toArray()[0]; assert.equal(input, s); - // every buffer cell index contains 2 string indices + // 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); + 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)); + const s = terminal.buffer.contents(true).toArray()[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); + 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)); + const s = terminal.buffer.contents(true).toArray()[0]; assert.equal(input, s); - // every buffer cell index contains 3 string indices + // 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); + 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)); + const s = terminal.buffer.contents(true).toArray()[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); + 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)); @@ -474,23 +467,23 @@ describe('Buffer', () => { // 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); + 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)); + const s = terminal.buffer.contents(true).toArray()[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); + 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)); + const s = terminal.buffer.contents(true).toArray()[0]; assert.equal(input, s); const stringIndex = s.match(/😃/).index; const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); diff --git a/src/Buffer.ts b/src/Buffer.ts index cb7d3b87..bc146a15 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,7 +4,7 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; @@ -194,12 +194,12 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; } - public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex { if (!stringIndex) { return [lineIndex, 0]; } while (stringIndex) { - let line = this.lines.get(lineIndex); + const line = this.lines.get(lineIndex); if (!line) { [-1, -1]; } @@ -360,6 +360,10 @@ export class Buffer implements IBuffer { // TODO: This could probably be optimized by relying on sort order and trimming the array using .length this.markers.splice(this.markers.indexOf(marker), 1); } + + public contents(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { + return new BufferStringIterator(this, trimRight, startIndex, endIndex); + } } export class Marker extends EventEmitter implements IMarker { @@ -386,3 +390,34 @@ export class Marker extends EventEmitter implements IMarker { super.dispose(); } } + +export class BufferStringIterator implements IBufferStringIterator { + private _start: number; + private _end: number; + private _current: number; + constructor (private _buffer: IBuffer, private _trimRight: boolean, startIndex?: number, endIndex?: number) { + this._start = startIndex || 0; + this._end = endIndex || this._buffer.lines.length; + this._current = this._start; + } + public hasNext(): boolean { + return this._current < this._end; + } + public next(withRanges: boolean = false): string | [{first: number, last: number}, string] { + const range = this._buffer.getWrappedRangeForLine(this._current); + let result = ''; + for (let i = range.first; i <= range.last; ++i) { + result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false); + } + this._current = range.last; + this._current++; + return (withRanges) ? [range, result] : result; + } + toArray(): string[] { + const result: string[] = []; + while (this.hasNext()) { + result.push(this.next() as string); + } + return result; + } +} diff --git a/src/Types.ts b/src/Types.ts index 820a8b89..5782f2c9 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -19,6 +19,9 @@ 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]; + export const enum LinkHoverEventTypes { HOVER = 'linkhover', TOOLTIP = 'linktooltip', @@ -265,6 +268,12 @@ export interface ITerminalOptions extends IPublicTerminalOptions { useFlowControl?: boolean; } +export interface IBufferStringIterator { + hasNext(): boolean; + next(withRanges: boolean): string | [{first: number, last: number}, string]; + toArray(): string[]; +} + export interface IBuffer { readonly lines: ICircularList; ydisp: number; @@ -283,6 +292,7 @@ export interface IBuffer { nextStop(x?: number): number; prevStop(x?: number): number; stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + contents(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator; } export interface IBufferSet extends IEventEmitter {