diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 5724715f45..b0075d8862 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -56,25 +56,25 @@ describe('Terminal', () => { // term.handler('fake'); // }); it('should fire the onCursorMove event', () => { - return new Promise(async r => { + return new Promise(async r => { term.onCursorMove(() => r()); await term.writeP('foo'); }); }); it('should fire the onLineFeed event', () => { - return new Promise(async r => { + return new Promise(async r => { term.onLineFeed(() => r()); await term.writeP('\n'); }); }); it('should fire a scroll event when scrollback is created', () => { - return new Promise(async r => { + return new Promise(async r => { term.onScroll(() => r()); await term.writeP('\n'.repeat(INIT_ROWS)); }); }); it('should fire a scroll event when scrollback is cleared', () => { - return new Promise(async r => { + return new Promise(async r => { await term.writeP('\n'.repeat(INIT_ROWS)); term.onScroll(() => r()); term.clear(); @@ -233,7 +233,7 @@ describe('Terminal', () => { term.paste('\r\nfoo\nbar\r'); }); it('should respect bracketed paste mode', () => { - return new Promise(async r => { + return new Promise(async r => { term.onData(e => { assert.equal(e, '\x1b[200~foo\x1b[201~'); r(); @@ -1054,10 +1054,10 @@ describe('Terminal', () => { linkifier.attachToDom({} as any, mouseZoneManager); }); - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[]): Promise { + function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise { return new Promise(async r => { await terminal.writeP(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); + linkifier.registerLinkMatcher(linkMatcherRegex, () => { }); linkifier.linkifyRows(); // Allow linkify to happen setTimeout(() => { @@ -1075,66 +1075,66 @@ describe('Terminal', () => { describe('unicode before the match', () => { it('combining - match within one line', () => { - return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); }); it('combining - match over two lines', () => { - return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); it('surrogate - match within one line', () => { - return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); }); it('surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); it('combining surrogate - match within one line', () => { - return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); }); it('combining surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); it('fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); it('fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); it('combining fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); it('combining fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); }); describe('unicode within the match', () => { it('combining - match within one line', () => { - return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); }); it('combining - match over two lines', () => { - return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); it('surrogate - match within one line', () => { - return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); it('surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); }); it('combining surrogate - match within one line', () => { - return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); it('combining surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); }); it('fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); }); it('fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); it('combining fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); + return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); }); it('combining fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); + return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); }); }); @@ -1143,7 +1143,7 @@ describe('Terminal', () => { let terminal: TestTerminal; beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); }); it('multiline ascii', async () => { @@ -1318,7 +1318,7 @@ describe('Terminal', () => { it('test fully wrapped buffer up to last char with full width odd', async () => { const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' - + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; + + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); @@ -1342,9 +1342,9 @@ describe('Terminal', () => { }); }); - describe('BufferStringIterator', function(): void { + describe('BufferStringIterator', function (): void { it('iterator does not overflow buffer limits', async () => { - const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); const data = [ 'aaaaaaaaaa', 'aaaaaaaaa\n', @@ -1382,13 +1382,13 @@ describe('Terminal', () => { 'aaaaaaaaa' // not wrapped ]; - const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); await normalTerminal.writeP(data.join('')); assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); - const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); + const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true }); await windowsModeTerminal.writeP(data.join('')); assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); @@ -1402,13 +1402,13 @@ describe('Terminal', () => { 'aaaaaaaaa' // not wrapped ]; - const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); await normalTerminal.writeP(data.join('')); assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); - const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); + const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true }); await windowsModeTerminal.writeP(data.join('')); assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); @@ -1417,7 +1417,7 @@ describe('Terminal', () => { }); it('convertEol setting', async () => { // not converting - const termNotConverting = new TestTerminal({cols: 15, rows: 10}); + const termNotConverting = new TestTerminal({ cols: 15, rows: 10 }); await termNotConverting.writeP('Hello\nWorld'); assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(false), 'Hello '); assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(false), ' World '); @@ -1425,128 +1425,13 @@ describe('Terminal', () => { assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World'); // converting - const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true}); + const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true }); await termConverting.writeP('Hello\nWorld'); assert.equal(termConverting.buffer.lines.get(0)!.translateToString(false), 'Hello '); assert.equal(termConverting.buffer.lines.get(1)!.translateToString(false), 'World '); assert.equal(termConverting.buffer.lines.get(0)!.translateToString(true), 'Hello'); assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World'); }); - describe('Terminal InputHandler integration', () => { - function getLines(term: TestTerminal, limit: number = term.rows): string[] { - const res: string[] = []; - for (let i = 0; i < limit; ++i) { - res.push(term.buffer.lines.get(i)!.translateToString(true)); - } - return res; - } - - // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService - describe('SL/SR/DECIC/DECDC', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); - }); - it('SL (scrollLeft)', async () => { - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[ @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']); - await term.writeP('\x1b[0 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']); - await term.writeP('\x1b[2 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']); - }); - it('SR (scrollRight)', async () => { - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[ A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); - await term.writeP('\x1b[0 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - await term.writeP('\x1b[2 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); - }); - it('insertColumns (DECIC)', async () => { - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[3;3H'); - await term.writeP('\x1b[\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - term.reset(); - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[3;3H'); - await term.writeP('\x1b[1\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - term.reset(); - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[3;3H'); - await term.writeP('\x1b[2\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); - }); - it('deleteColumns (DECDC)', async () => { - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[3;3H'); - await term.writeP('\x1b[\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); - term.reset(); - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[3;3H'); - await term.writeP('\x1b[1\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); - term.reset(); - await term.writeP('12345'.repeat(6)); - await term.writeP('\x1b[3;3H'); - await term.writeP('\x1b[2\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']); - }); - }); - - describe('BS with reverseWraparound set/unset', () => { - const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS - - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); - }); - - describe('reverseWraparound set', () => { - it('should not reverse outside of scroll margins', async () => { - // prepare buffer content - await term.writeP('#####abcdefghijklmnopqrstuvwxy'); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); - assert.equal(term.buffer.ydisp, 1); - assert.equal(term.buffer.x, 5); - assert.equal(term.buffer.y, 4); - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); - - await term.writeP('\x1b[?45h'); - await term.writeP('uvwxy'); - - // set top/bottom to 1/3 (0-based) - await term.writeP('\x1b[2;4r'); - // place cursor below scroll bottom - term.buffer.x = 5; - term.buffer.y = 4; - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); - - await term.writeP('uvwxy'); - // place cursor within scroll margins - term.buffer.x = 5; - term.buffer.y = 3; - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); - assert.equal(term.buffer.x, 0); - assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop - - await term.writeP('fghijklmnopqrst'); - // place cursor above scroll top - term.buffer.x = 5; - term.buffer.y = 0; - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); - }); - }); - }); - }); // FIXME: move to common/CoreTerminal.test once the trimming is moved over describe('marker lifecycle', () => { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 1ab207c7f5..f14bffe074 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -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)); @@ -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; } diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 936aa0c8d9..699a1b1c49 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -58,8 +58,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected _inputHandler: InputHandler; private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - /** An IBufferline to clone/copy from for new blank lines */ - private _cachedBlankLine: IBufferLine | undefined; private _onBinary = new EventEmitter(); public get onBinary(): IEvent { return this._onBinary.event; } @@ -123,6 +121,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)); @@ -174,97 +176,18 @@ 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; - 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); } /** @@ -272,28 +195,25 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * @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. */ diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index b5067193d7..cb60aec3da 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -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 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; @@ -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)); @@ -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 () => { @@ -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()); @@ -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 @@ -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'); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 13b0a0e7cc..3af2f9d741 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -240,8 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler { public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onRequestReset = new EventEmitter(); public get onRequestReset(): IEvent { return this._onRequestReset.event; } - private _onRequestScroll = new EventEmitter(); - public get onRequestScroll(): IEvent { return this._onRequestScroll.event; } private _onRequestSyncScrollBar = new EventEmitter(); public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; } private _onRequestWindowsOptionsReport = new EventEmitter(); @@ -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; @@ -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; } @@ -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; } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 01ceacbd24..dce0f570a6 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -9,7 +9,7 @@ import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; export class MockBufferService implements IBufferService { @@ -17,6 +17,7 @@ export class MockBufferService implements IBufferService { public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event; + public onScroll: IEvent = new EventEmitter().event; public isUserScrolling: boolean = false; constructor( public cols: number, @@ -25,23 +26,41 @@ export class MockBufferService implements IBufferService { ) { this.buffers = new BufferSet(optionsService, this); } + public scrollPages(pageCount: number): void { + throw new Error('Method not implemented.'); + } + public scrollToTop(): void { + throw new Error('Method not implemented.'); + } + public scrollToLine(line: number): void { + throw new Error('Method not implemented.'); + } + public scroll(eraseAttr: IAttributeData, isWrapped: boolean): void { + throw new Error('Method not implemented.'); + } + public scrollToBottom(): void { + throw new Error('Method not implemented.'); + } + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + throw new Error('Method not implemented.'); + } public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; } - public reset(): void {} + public reset(): void { } } export class MockCoreMouseService implements ICoreMouseService { public areMouseEventsActive: boolean = false; public activeEncoding: string = ''; public activeProtocol: string = ''; - public addEncoding(name: string): void {} - public addProtocol(name: string): void {} - public reset(): void {} + public addEncoding(name: string): void { } + public addProtocol(name: string): void { } + public reset(): void { } public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } public onProtocolChange: IEvent = new EventEmitter().event; - public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { + public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } } @@ -50,9 +69,9 @@ export class MockCharsetService implements ICharsetService { public serviceBrand: any; public charset: ICharset | undefined; public glevel: number = 0; - public reset(): void {} - public setgLevel(g: number): void {} - public setgCharset(g: number, charset: ICharset): void {} + public reset(): void { } + public setgLevel(g: number): void { } + public setgCharset(g: number, charset: ICharset): void { } } export class MockCoreService implements ICoreService { @@ -75,28 +94,28 @@ export class MockCoreService implements ICoreService { public onData: IEvent = new EventEmitter().event; public onUserInput: IEvent = new EventEmitter().event; public onBinary: IEvent = new EventEmitter().event; - public reset(): void {} - public triggerDataEvent(data: string, wasUserInput?: boolean): void {} - public triggerBinaryEvent(data: string): void {} + public reset(): void { } + public triggerDataEvent(data: string, wasUserInput?: boolean): void { } + public triggerBinaryEvent(data: string): void { } } export class MockDirtyRowService implements IDirtyRowService { public serviceBrand: any; public start: number = 0; public end: number = 0; - public clearRange(): void {} - public markDirty(y: number): void {} - public markRangeDirty(y1: number, y2: number): void {} - public markAllDirty(): void {} + public clearRange(): void { } + public markDirty(y: number): void { } + public markRangeDirty(y1: number, y2: number): void { } + public markAllDirty(): void { } } export class MockLogService implements ILogService { public serviceBrand: any; public logLevel = LogLevelEnum.DEBUG; - public debug(message: any, ...optionalParams: any[]): void {} - public info(message: any, ...optionalParams: any[]): void {} - public warn(message: any, ...optionalParams: any[]): void {} - public error(message: any, ...optionalParams: any[]): void {} + public debug(message: any, ...optionalParams: any[]): void { } + public info(message: any, ...optionalParams: any[]): void { } + public warn(message: any, ...optionalParams: any[]): void { } + public error(message: any, ...optionalParams: any[]): void { } } export class MockOptionsService implements IOptionsService { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2dd3f4b8ed..df29919560 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -357,7 +357,6 @@ export interface IAnsiColorChangeEvent { */ export interface IInputHandler { onTitleChange: IEvent; - onRequestScroll: IEvent; parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise; print(data: Uint32Array, start: number, end: number): void; diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 47e547290d..99594d228e 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -8,6 +8,7 @@ import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; +import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; @@ -23,9 +24,14 @@ export class BufferService extends Disposable implements IBufferService { private _onResize = new EventEmitter<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + private _onScroll = new EventEmitter(); + public get onScroll(): IEvent { return this._onScroll.event; } public get buffer(): IBuffer { return this.buffers.active; } + /** An IBufferline to clone/copy from for new blank lines */ + private _cachedBlankLine: IBufferLine | undefined; + constructor( @IOptionsService private _optionsService: IOptionsService ) { @@ -52,4 +58,128 @@ export class BufferService extends Disposable implements IBufferService { this.buffers.reset(); this.isUserScrolling = false; } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { + const buffer = this.buffer; + + let newLine: IBufferLine | undefined; + newLine = this._cachedBlankLine; + 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.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.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.isUserScrolling) { + buffer.ydisp = buffer.ybase; + } + + this._onScroll.fire(buffer.ydisp); + } + + /** + * Scroll the display of the terminal + * @param disp The number of lines to scroll down (negative scroll up). + * @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?: boolean, source?: ScrollSource): void { + const buffer = this.buffer; + if (disp < 0) { + if (buffer.ydisp === 0) { + return; + } + this.isUserScrolling = true; + } else if (disp + buffer.ydisp >= buffer.ybase) { + this.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(buffer.ydisp); + } + } + + /** + * 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)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollLines(-this.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollLines(this.buffer.ybase - this.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 1fbf57fbe6..8b21f100ac 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -17,9 +17,14 @@ export interface IBufferService { readonly buffer: IBuffer; readonly buffers: IBufferSet; isUserScrolling: boolean; - onResize: IEvent<{ cols: number, rows: number }>; - + onScroll: IEvent; + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; + scrollToBottom(): void; + scrollToTop(): void; + scrollToLine(line: number): void; + scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void; + scrollPages(pageCount: number): void; resize(cols: number, rows: number): void; reset(): void; }