Merge branch 'master' into webgl

This commit is contained in:
Megan Rogge
2021-04-01 16:57:21 -07:00
committed by GitHub
9 changed files with 344 additions and 283 deletions
+37 -152
View File
@@ -56,25 +56,25 @@ describe('Terminal', () => {
// term.handler('fake');
// });
it('should fire the onCursorMove event', () => {
return new Promise(async r => {
return new Promise<void>(async r => {
term.onCursorMove(() => r());
await term.writeP('foo');
});
});
it('should fire the onLineFeed event', () => {
return new Promise(async r => {
return new Promise<void>(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<void>(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<void>(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<void>(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<void> {
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise<void> {
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 ab', /ab/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
return assertLinkifiesInTerminal('test ab', /ab/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]);
});
it('fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('testtest ab', /ab/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
return assertLinkifiesInTerminal('testtest ab', /ab/, [{ 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 <BS SP BS> 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', () => {
+1 -2
View File
@@ -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;
}
+14 -94
View File
@@ -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<string>();
public get onBinary(): IEvent<string> { 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. */
+112 -6
View File
@@ -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;
@@ -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');
+3 -5
View File
@@ -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>();
@@ -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;
}
+39 -20
View File
@@ -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<number> = new EventEmitter<number>().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<CoreMouseEventType> = new EventEmitter<CoreMouseEventType>().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<string> = new EventEmitter<string>().event;
public onUserInput: IEvent<void> = new EventEmitter<void>().event;
public onBinary: IEvent<string> = new EventEmitter<string>().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 {
-1
View File
@@ -357,7 +357,6 @@ export interface IAnsiColorChangeEvent {
*/
export interface IInputHandler {
onTitleChange: IEvent<string>;
onRequestScroll: IEvent<IAttributeData, boolean | void>;
parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>;
print(data: Uint32Array, start: number, end: number): void;
+130
View File
@@ -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<number>();
public get onScroll(): IEvent<number> { 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);
}
}
}
+8 -3
View File
@@ -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<IBufferService>('BufferService');
@@ -17,9 +17,14 @@ export interface IBufferService {
readonly buffer: IBuffer;
readonly buffers: IBufferSet;
isUserScrolling: boolean;
onResize: IEvent<{ cols: number, rows: number }>;
onScroll: IEvent<number>;
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;
}