Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 37 additions & 152 deletions src/browser/Terminal.test.ts

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions src/browser/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._inputHandler.onRequestBell(() => this.bell()));
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
this.register(this._inputHandler.onRequestReset(() => this.reset()));
this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined)));
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
Expand Down Expand Up @@ -1010,7 +1009,7 @@ export class Terminal extends CoreTerminal implements ITerminal {

if (!this._compositionHelper!.keydown(event)) {
if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
this._bufferService.scrollToBottom();
}
return false;
}
Expand Down
106 changes: 14 additions & 92 deletions src/common/CoreTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(forwardEvent(this._coreService.onData, this._onData));
this.register(forwardEvent(this._coreService.onBinary, this._onBinary));
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
this.register(this._bufferService.onScroll(event => {
this._onScroll.fire({position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL});
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));

// Setup WriteBuffer
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
Expand Down Expand Up @@ -174,126 +178,44 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
const buffer = this._bufferService.buffer;

let newLine: IBufferLine | undefined;
newLine = this._cachedBlankLine;
Comment thread
Tyriar marked this conversation as resolved.
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
this._cachedBlankLine = newLine;
}
newLine.isWrapped = isWrapped;

const topRow = buffer.ybase + buffer.scrollTop;
const bottomRow = buffer.ybase + buffer.scrollBottom;

if (buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = buffer.lines.isFull;

// Insert the line using the fastest method
if (bottomRow === buffer.lines.length - 1) {
if (willBufferBeTrimmed) {
buffer.lines.recycle().copyFrom(newLine);
} else {
buffer.lines.push(newLine.clone());
}
} else {
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
}

// Only adjust ybase and ydisp when the buffer is not trimmed
if (!willBufferBeTrimmed) {
buffer.ybase++;
// Only scroll the ydisp with ybase if the user has not scrolled up
if (!this._bufferService.isUserScrolling) {
buffer.ydisp++;
}
} else {
// When the buffer is full and the user has scrolled up, keep the text
// stable unless ydisp is right at the top
if (this._bufferService.isUserScrolling) {
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
}
}
} else {
// scrollTop is non-zero which means no line will be going to the
// scrollback, instead we can just shift them in-place.
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
buffer.lines.set(bottomRow, newLine.clone());
}

// Move the viewport to the bottom of the buffer unless the user is
// scrolling.
if (!this._bufferService.isUserScrolling) {
buffer.ydisp = buffer.ybase;
}

// Flag rows that need updating
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);

this._onScroll.fire({ position: buffer.ydisp, source: ScrollSource.TERMINAL });
this._bufferService.scroll(eraseAttr, isWrapped);
}

/**
* Scroll the display of the terminal
* @param disp The number of lines to scroll down (negative scroll up).
* @param suppressScrollEvent Don't emit an onScroll event.
* @param source The source of the scroll action. Emitted as part of the onScroll event
* to avoid cyclic invocations if the event originated from the Viewport.
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
* to avoid unwanted events being handled by the viewport when the event was triggered from the
* viewport originally.
*/
public scrollLines(disp: number, suppressScrollEvent = false, source = ScrollSource.TERMINAL): void {
const buffer = this._bufferService.buffer;
if (disp < 0) {
if (buffer.ydisp === 0) {
return;
}
this._bufferService.isUserScrolling = true;
} else if (disp + buffer.ydisp >= buffer.ybase) {
this._bufferService.isUserScrolling = false;
}

const oldYdisp = buffer.ydisp;
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);

// No change occurred, don't trigger scroll/refresh
if (oldYdisp === buffer.ydisp) {
return;
}

if (!suppressScrollEvent) {
this._onScroll.fire({ position: buffer.ydisp, source });
}
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
this._bufferService.scrollLines(disp, suppressScrollEvent, source);
}

/**
* Scroll the display of the terminal by a number of pages.
* @param pageCount The number of pages to scroll (negative scrolls up).
*/
public scrollPages(pageCount: number): void {
this.scrollLines(pageCount * (this.rows - 1));
this._bufferService.scrollPages(pageCount);
}

/**
* Scrolls the display of the terminal to the top.
*/
public scrollToTop(): void {
this.scrollLines(-this._bufferService.buffer.ydisp);
this._bufferService.scrollToTop();
}

/**
* Scrolls the display of the terminal to the bottom.
*/
public scrollToBottom(): void {
this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
this._bufferService.scrollToBottom();
}

public scrollToLine(line: number): void {
const scrollAmount = line - this._bufferService.buffer.ydisp;
if (scrollAmount !== 0) {
this.scrollLines(scrollAmount);
}
this._bufferService.scrollToLine(line);
}

/** Add handler for ESC escape sequence. See xterm.d.ts for details. */
Expand Down
118 changes: 112 additions & 6 deletions src/common/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,117 @@ describe('InputHandler', () => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService);
bufferService.resize(80, 30);
coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService);
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);

inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
});

describe('SL/SR/DECIC/DECDC', () => {
beforeEach(() => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
bufferService.reset();
});
it('SL (scrollLeft)', async () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[ @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']);
inputHandler.parseP('\x1b[0 @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']);
inputHandler.parseP('\x1b[2 @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']);
});
it('SR (scrollRight)', async () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[ A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
inputHandler.parseP('\x1b[0 A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
inputHandler.parseP('\x1b[2 A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
});
it('insertColumns (DECIC)', async () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[1\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[2\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
});
it('deleteColumns (DECDC)', async () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[1\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[2\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']);
});
});

describe('BS with reverseWraparound set/unset', () => {
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
beforeEach(() => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
bufferService.reset();
});
describe('reverseWraparound set', () => {
it('should not reverse outside of scroll margins', async () => {
// prepare buffer content
inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy');
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
assert.equal(bufferService.buffers.active.ydisp, 1);
assert.equal(bufferService.buffers.active.x, 5);
assert.equal(bufferService.buffers.active.y, 4);
inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);

inputHandler.parseP('\x1b[?45h');
inputHandler.parseP('uvwxy');

// set top/bottom to 1/3 (0-based)
inputHandler.parseP('\x1b[2;4r');
// place cursor below scroll bottom
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 4;
inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);

inputHandler.parseP('uvwxy');
// place cursor within scroll margins
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 3;
inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
assert.equal(bufferService.buffers.active.x, 0);
assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop

inputHandler.parseP('fghijklmnopqrst');
// place cursor above scroll top
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 0;
inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
});
});
});

it('save and restore cursor', () => {
bufferService.buffer.x = 1;
bufferService.buffer.y = 2;
Expand Down Expand Up @@ -140,7 +246,7 @@ describe('InputHandler', () => {
assert.equal(coreService.decPrivateModes.bracketedPasteMode, false);
});
});
describe('regression tests', function(): void {
describe('regression tests', function (): void {
function termContent(bufferService: IBufferService, trim: boolean): string[] {
const result = [];
for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim));
Expand Down Expand Up @@ -1240,7 +1346,7 @@ describe('InputHandler', () => {
await inputHandler.parseP('\x1b[6H\x1b[2Mm');
assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']);
await inputHandler.parseP('\x1b[3H\x1b[2Mn');
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
});
});
it('should parse big chunks in smaller subchunks', async () => {
Expand Down Expand Up @@ -1820,7 +1926,7 @@ describe('InputHandler - async handlers', () => {
optionsService = new MockOptionsService();
bufferService = new BufferService(optionsService);
bufferService.resize(80, 30);
coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService);
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });

inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
Expand All @@ -1829,7 +1935,7 @@ describe('InputHandler - async handlers', () => {
it('async CUP with CPR check', async () => {
const cup: number[][] = [];
const cpr: number[][] = [];
inputHandler.registerCsiHandler({final: 'H'}, async params => {
inputHandler.registerCsiHandler({ final: 'H' }, async params => {
cup.push(params.toArray() as number[]);
await new Promise(res => setTimeout(res, 50));
// late call of real repositioning
Expand All @@ -1855,7 +1961,7 @@ describe('InputHandler - async handlers', () => {
assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
});
it('async DCS between', async () => {
inputHandler.registerDcsHandler({final: 'a'}, async (data, params) => {
inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
await new Promise(res => setTimeout(res, 50));
assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
assert.equal(data, 'some data');
Expand Down
8 changes: 3 additions & 5 deletions src/common/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler {
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
private _onRequestReset = new EventEmitter<void>();
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
private _onRequestScroll = new EventEmitter<IAttributeData, boolean | void>();
public get onRequestScroll(): IEvent<IAttributeData, boolean | void> { return this._onRequestScroll.event; }
private _onRequestSyncScrollBar = new EventEmitter<void>();
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
Expand Down Expand Up @@ -651,7 +649,7 @@ export class InputHandler extends Disposable implements IInputHandler {
buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._onRequestScroll.fire(this._eraseAttrData(), true);
this._bufferService.scroll(this._eraseAttrData(), true);
} else {
if (buffer.y >= this._bufferService.rows) {
buffer.y = this._bufferService.rows - 1;
Expand Down Expand Up @@ -791,7 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler {
buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._onRequestScroll.fire(this._eraseAttrData());
this._bufferService.scroll(this._eraseAttrData());
} else if (buffer.y >= this._bufferService.rows) {
buffer.y = this._bufferService.rows - 1;
}
Expand Down Expand Up @@ -2987,7 +2985,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._bufferService.buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._onRequestScroll.fire(this._eraseAttrData());
this._bufferService.scroll(this._eraseAttrData());
} else if (buffer.y >= this._bufferService.rows) {
buffer.y = this._bufferService.rows - 1;
}
Expand Down
Loading