diff --git a/src/Buffer.ts b/src/Buffer.ts index fd6f8aef..7af090ac 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -78,10 +78,9 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.scrollBottom = 0; - this.scrollTop = 0; this.tabs = {}; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 771835bb..0c333741 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -3,6 +3,9 @@ import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; +const INIT_COLS = 80; +const INIT_ROWS = 24; + class TestTerminal extends Terminal { public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollDisp: number} { return this._evaluateKeyEscapeSequence(ev); } public keyDown(ev: any): boolean { return this._keyDown(ev); } @@ -13,7 +16,10 @@ describe('term.js addons', () => { let term: TestTerminal; beforeEach(() => { - term = new TestTerminal(); + term = new TestTerminal({ + cols: INIT_COLS, + rows: INIT_ROWS + }); term.refresh = () => {}; term.viewport = new MockViewport(); (term).compositionHelper = new MockCompositionHelper(); @@ -283,6 +289,138 @@ describe('term.js addons', () => { assert.equal(term.buffer.ydisp, startYDisp - 1); }); }); + + describe('scroll() function', () => { + describe('when scrollback > 0', () => { + it('should create a new line and scroll', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS + 1); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' '); + }); + + it('should properly scroll inside a scroll region (scrollTop set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + }); + + it('should properly scroll inside a scroll region (scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = 3; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS + 1); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + + it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + }); + + describe('when scrollback === 0', () => { + beforeEach(() => { + term.setOption('scrollback', 0); + assert.equal(term.buffer.lines.maxLength, INIT_ROWS); + }); + + it('should create a new line and shift everything up', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + assert.equal(term.buffer.lines.length, INIT_ROWS); + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + // 'a' gets pushed out of buffer + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' '); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' '); + }); + + it('should properly scroll inside a scroll region (scrollTop set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + }); + + it('should properly scroll inside a scroll region (scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = 3; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + + it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + }); + }); }); describe('evaluateKeyEscapeSequence', () => { diff --git a/src/Terminal.ts b/src/Terminal.ts index 03babac5..a083d9d4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1167,46 +1167,104 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * line. */ public scroll(isWrapped?: boolean): void { - let row; + // The problem is caused when scrollback = 0. This section was written + // around the assumption that there would be buffer to trim when necessary. - // Make room for the new row in lines - const bufferNeedsTrimming = this.buffer.lines.length === this.buffer.lines.maxLength; - if (bufferNeedsTrimming) { - this.buffer.lines.trimStart(1); - this.buffer.ybase--; - this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); + + // shiftElements should be used here + + // If scrollTop is set (non-zero), we should shiftElements + // scrollBottom defines the position at which the new element is inserted + + // Note that both scrollBottom and scrollTop need to be handled individually + // because they can be set to terminal.rows or 0 respectively + + // Don't mess with ybase temporarily, asking for trouble + + // This should work with minimal effort utilizing the CiruclarList + + // TODO: Need to hold onto top given the if? + const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; + console.log('bottomRow', bottomRow); + console.log('this.buffer.scrollTop: ' + this.buffer.scrollTop); + if (this.buffer.scrollTop === 0) { + const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength; + + if (bottomRow === this.buffer.lines.length - 1) { + // Pushing when possible is faster than splicing + this.buffer.lines.push(this.blankLine(undefined, isWrapped)); + console.log('push'); + } else { + // Insert a row *below* the bottomRow, pushing the top row into the scrollback + this.buffer.lines.splice(bottomRow + 1, 0, this.blankLine(undefined, isWrapped)); + console.log('splice'); + } + + // Adjust ydisp and ybase to accommodate the changes after the buffer was + // trimmed. + if (!willBufferBeTrimmed) { + console.log('increment ydisp/ybase'); + this.buffer.ybase++; + this.buffer.ydisp++; + } + console.log('this.buffer.ybase: ' + this.buffer.ybase); + console.log('this.buffer.ybase: ' + this.buffer.ydisp); + } else { + const topRow = this.buffer.ybase + this.buffer.scrollTop; + const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; +console.log('shiftElements'); +console.log('topRow: ' + topRow + '(' + this.buffer.lines.get(topRow)[0][1] + ')'); +console.log('scrollRegionHeight: ' + scrollRegionHeight); + this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + this.buffer.lines.set(bottomRow, this.blankLine(undefined, isWrapped)); } - this.buffer.ybase++; - - // Scroll the viewport down to the bottom if the user is not scrolling if (!this.userScrolling) { this.buffer.ydisp = this.buffer.ybase; } - // last line - row = this.buffer.ybase + this.rows - 1; - // subtract the bottom scroll region - row -= this.rows - 1 - this.buffer.scrollBottom; + // Make room for the new row in lines + // const bufferNeedsTrimming = this.buffer.lines.length === this.buffer.lines.maxLength; + // if (bufferNeedsTrimming) { + // this.buffer.lines.trimStart(1); + // this.buffer.ybase--; + // this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); + // } - if (row === this.buffer.lines.length) { - // Optimization: pushing is faster than splicing when they amount to the same behavior - this.buffer.lines.push(this.blankLine(undefined, isWrapped)); - } else { - // add our new line - this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); - } + // this.buffer.ybase++; - if (this.buffer.scrollTop !== 0) { - if (this.buffer.ybase !== 0) { - this.buffer.ybase--; - if (!this.userScrolling) { - this.buffer.ydisp = this.buffer.ybase; - } - } - this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); - } + // // Scroll the viewport down to the bottom if the user is not scrolling + // if (!this.userScrolling) { + // this.buffer.ydisp = this.buffer.ybase; + // } + + // // last line + // let row = this.buffer.ybase + this.rows - 1; + + // // subtract the bottom scroll region + // row -= this.rows - 1 - this.buffer.scrollBottom; + + // // Same as this? + // // row = this.buffer.ybase + this.buffer.scrollBottom; + + // if (row === this.buffer.lines.length) { + // // Optimization: pushing is faster than splicing when they amount to the same behavior + // this.buffer.lines.push(this.blankLine(undefined, isWrapped)); + // } else { + // // add our new line + // this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); + // } + + // if (this.buffer.scrollTop !== 0) { + // if (this.buffer.ybase !== 0) { + // this.buffer.ybase--; + // if (!this.userScrolling) { + // this.buffer.ydisp = this.buffer.ybase; + // } + // } + // this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); + // } // this.maxRange(); this.updateRange(this.buffer.scrollTop);