diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 4c348ec6..7abf6f72 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -392,6 +392,124 @@ describe('Buffer', () => { 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, 15); + 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: + // 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, 15); + 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, 15); + 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, 10); + 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: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + buffer.y = 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, 10); + 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, 10); + 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'); + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 27662708..5b9af742 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CircularList } from './common/CircularList'; +import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; @@ -238,7 +238,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this.hasScrollback && this._bufferLineConstructor === BufferLine) { + if (this._hasScrollback && this._bufferLineConstructor === BufferLine) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -338,6 +338,13 @@ export class Buffer implements IBuffer { for (let i = 0; i < this.lines.length; i++) { if (nextToRemoveStart === i) { const countToRemove = toRemove[++nextToRemoveIndex]; + + // Tell markers that there was a deletion + this.lines.emit('delete', { + index: i - countRemovedSoFar, + amount: countToRemove + } as IDeleteEvent); + i += countToRemove - 1; countRemovedSoFar += countToRemove; nextToRemoveStart = toRemove[++nextToRemoveIndex]; @@ -474,6 +481,10 @@ export class Buffer implements IBuffer { // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many // costly calls to CircularList.splice. if (toInsert.length > 0) { + // Record buffer insert events and then play them back backwards so that the indexes are + // correct + const insertEvents: IInsertEvent[] = []; + // Record original lines so they don't get overridden when we rearrange the list const originalLines: BufferLine[] = []; for (let i = 0; i < this.lines.length; i++) { @@ -492,14 +503,32 @@ export class Buffer implements IBuffer { for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { this.lines.set(i--, nextToInsert.newLines[nextI]); } - i++; // Don't skip for the first row + i++; + + // Create insert events for later + insertEvents.push({ + index: originalLineIndex + 1, + amount: nextToInsert.newLines.length + } as IInsertEvent); + countInsertedSoFar += nextToInsert.newLines.length; nextToInsert = toInsert[++nextToInsertIndex]; } else { this.lines.set(i, originalLines[originalLineIndex--]); } } - // TODO: Throw trim event + + // Update markers + let insertCountEmitted = 0; + for (let i = insertEvents.length - 1; i >= 0; i--) { + insertEvents[i].index += insertCountEmitted; + this.lines.emit('insert', insertEvents[i]); + insertCountEmitted += insertEvents[i].amount; + } + const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); + if (amountToTrim > 0) { + this.lines.emitMayRemoveListeners('trim', amountToTrim); + } } } @@ -618,12 +647,27 @@ export class Buffer implements IBuffer { marker.dispose(); } })); + marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => { + if (marker.line >= event.index) { + marker.line += event.amount; + } + })); + marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => { + // Delete the marker if it's within the range + if (marker.line >= event.index && marker.line < event.index + event.amount) { + marker.dispose(); + } + + // Shift the marker if it's after the deleted range + if (marker.line > event.index) { + marker.line -= event.amount; + } + })); marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker))); return marker; } private _removeMarker(marker: Marker): void { - // 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); } diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 9faf534a..af4d8af5 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -6,6 +6,16 @@ import { EventEmitter } from './EventEmitter'; import { ICircularList } from './Types'; +export interface IInsertEvent { + index: number; + amount: number; +} + +export interface IDeleteEvent { + index: number; + amount: number; +} + /** * Represents a circular list; a list with a maximum size that wraps around when push is called, * overriding values at the start of the list. @@ -91,7 +101,7 @@ export class CircularList extends EventEmitter implements ICircularList { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); } else { this._length++; } @@ -107,7 +117,7 @@ export class CircularList extends EventEmitter implements ICircularList { throw new Error('Can only recycle when the buffer is full'); } this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); return this._array[this._getCyclicIndex(this._length - 1)]!; } @@ -158,7 +168,7 @@ export class CircularList extends EventEmitter implements ICircularList { const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; this._length = this._maxLength; - this.emit('trim', countToTrim); + this.emitMayRemoveListeners('trim', countToTrim); } else { this._length += items.length; } @@ -175,7 +185,7 @@ export class CircularList extends EventEmitter implements ICircularList { } this._startIndex += count; this._length -= count; - this.emit('trim', count); + this.emitMayRemoveListeners('trim', count); } public shiftElements(start: number, count: number, offset: number): void { @@ -199,7 +209,7 @@ export class CircularList extends EventEmitter implements ICircularList { while (this._length > this._maxLength) { this._length--; this._startIndex++; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); } } } else { diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index f9c0c001..c6b09901 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -75,6 +75,19 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab } } + public emitMayRemoveListeners(type: string, ...args: any[]): void { + if (!this._events[type]) { + return; + } + const obj = this._events[type]; + let length = obj.length; + for (let i = 0; i < obj.length; i++) { + obj[i].apply(this, args); + i -= length - obj.length; + length = obj.length; + } + } + public listeners(type: string): XtermListener[] { return this._events[type] || []; }