Skip to content

Commit 337c11d

Browse files
authored
Merge pull request #3277 from meganrogge/bufferService
Move scroll to BufferService
2 parents 759379d + ecb485d commit 337c11d

9 files changed

Lines changed: 344 additions & 283 deletions

File tree

src/browser/Terminal.test.ts

Lines changed: 37 additions & 152 deletions
Large diffs are not rendered by default.

src/browser/Terminal.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
147147
this.register(this._inputHandler.onRequestBell(() => this.bell()));
148148
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
149149
this.register(this._inputHandler.onRequestReset(() => this.reset()));
150-
this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined)));
151150
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
152151
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
153152
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
@@ -1010,7 +1009,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
10101009

10111010
if (!this._compositionHelper!.keydown(event)) {
10121011
if (this.buffer.ybase !== this.buffer.ydisp) {
1013-
this.scrollToBottom();
1012+
this._bufferService.scrollToBottom();
10141013
}
10151014
return false;
10161015
}

src/common/CoreTerminal.ts

Lines changed: 14 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
5858
protected _inputHandler: InputHandler;
5959
private _writeBuffer: WriteBuffer;
6060
private _windowsMode: IDisposable | undefined;
61-
/** An IBufferline to clone/copy from for new blank lines */
62-
private _cachedBlankLine: IBufferLine | undefined;
6361

6462
private _onBinary = new EventEmitter<string>();
6563
public get onBinary(): IEvent<string> { return this._onBinary.event; }
@@ -123,6 +121,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
123121
this.register(forwardEvent(this._coreService.onData, this._onData));
124122
this.register(forwardEvent(this._coreService.onBinary, this._onBinary));
125123
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
124+
this.register(this._bufferService.onScroll(event => {
125+
this._onScroll.fire({position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL});
126+
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
127+
}));
126128

127129
// Setup WriteBuffer
128130
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
@@ -174,126 +176,44 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
174176
* @param isWrapped Whether the new line is wrapped from the previous line.
175177
*/
176178
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
177-
const buffer = this._bufferService.buffer;
178-
179-
let newLine: IBufferLine | undefined;
180-
newLine = this._cachedBlankLine;
181-
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
182-
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
183-
this._cachedBlankLine = newLine;
184-
}
185-
newLine.isWrapped = isWrapped;
186-
187-
const topRow = buffer.ybase + buffer.scrollTop;
188-
const bottomRow = buffer.ybase + buffer.scrollBottom;
189-
190-
if (buffer.scrollTop === 0) {
191-
// Determine whether the buffer is going to be trimmed after insertion.
192-
const willBufferBeTrimmed = buffer.lines.isFull;
193-
194-
// Insert the line using the fastest method
195-
if (bottomRow === buffer.lines.length - 1) {
196-
if (willBufferBeTrimmed) {
197-
buffer.lines.recycle().copyFrom(newLine);
198-
} else {
199-
buffer.lines.push(newLine.clone());
200-
}
201-
} else {
202-
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
203-
}
204-
205-
// Only adjust ybase and ydisp when the buffer is not trimmed
206-
if (!willBufferBeTrimmed) {
207-
buffer.ybase++;
208-
// Only scroll the ydisp with ybase if the user has not scrolled up
209-
if (!this._bufferService.isUserScrolling) {
210-
buffer.ydisp++;
211-
}
212-
} else {
213-
// When the buffer is full and the user has scrolled up, keep the text
214-
// stable unless ydisp is right at the top
215-
if (this._bufferService.isUserScrolling) {
216-
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
217-
}
218-
}
219-
} else {
220-
// scrollTop is non-zero which means no line will be going to the
221-
// scrollback, instead we can just shift them in-place.
222-
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
223-
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
224-
buffer.lines.set(bottomRow, newLine.clone());
225-
}
226-
227-
// Move the viewport to the bottom of the buffer unless the user is
228-
// scrolling.
229-
if (!this._bufferService.isUserScrolling) {
230-
buffer.ydisp = buffer.ybase;
231-
}
232-
233-
// Flag rows that need updating
234-
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
235-
236-
this._onScroll.fire({ position: buffer.ydisp, source: ScrollSource.TERMINAL });
179+
this._bufferService.scroll(eraseAttr, isWrapped);
237180
}
238181

239182
/**
240183
* Scroll the display of the terminal
241184
* @param disp The number of lines to scroll down (negative scroll up).
242-
* @param suppressScrollEvent Don't emit an onScroll event.
243-
* @param source The source of the scroll action. Emitted as part of the onScroll event
244-
* to avoid cyclic invocations if the event originated from the Viewport.
185+
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
186+
* to avoid unwanted events being handled by the viewport when the event was triggered from the
187+
* viewport originally.
245188
*/
246-
public scrollLines(disp: number, suppressScrollEvent = false, source = ScrollSource.TERMINAL): void {
247-
const buffer = this._bufferService.buffer;
248-
if (disp < 0) {
249-
if (buffer.ydisp === 0) {
250-
return;
251-
}
252-
this._bufferService.isUserScrolling = true;
253-
} else if (disp + buffer.ydisp >= buffer.ybase) {
254-
this._bufferService.isUserScrolling = false;
255-
}
256-
257-
const oldYdisp = buffer.ydisp;
258-
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
259-
260-
// No change occurred, don't trigger scroll/refresh
261-
if (oldYdisp === buffer.ydisp) {
262-
return;
263-
}
264-
265-
if (!suppressScrollEvent) {
266-
this._onScroll.fire({ position: buffer.ydisp, source });
267-
}
189+
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
190+
this._bufferService.scrollLines(disp, suppressScrollEvent, source);
268191
}
269192

270193
/**
271194
* Scroll the display of the terminal by a number of pages.
272195
* @param pageCount The number of pages to scroll (negative scrolls up).
273196
*/
274197
public scrollPages(pageCount: number): void {
275-
this.scrollLines(pageCount * (this.rows - 1));
198+
this._bufferService.scrollPages(pageCount);
276199
}
277200

278201
/**
279202
* Scrolls the display of the terminal to the top.
280203
*/
281204
public scrollToTop(): void {
282-
this.scrollLines(-this._bufferService.buffer.ydisp);
205+
this._bufferService.scrollToTop();
283206
}
284207

285208
/**
286209
* Scrolls the display of the terminal to the bottom.
287210
*/
288211
public scrollToBottom(): void {
289-
this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
212+
this._bufferService.scrollToBottom();
290213
}
291214

292215
public scrollToLine(line: number): void {
293-
const scrollAmount = line - this._bufferService.buffer.ydisp;
294-
if (scrollAmount !== 0) {
295-
this.scrollLines(scrollAmount);
296-
}
216+
this._bufferService.scrollToLine(line);
297217
}
298218

299219
/** Add handler for ESC escape sequence. See xterm.d.ts for details. */

src/common/InputHandler.test.ts

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,117 @@ describe('InputHandler', () => {
6666
optionsService = new MockOptionsService();
6767
bufferService = new BufferService(optionsService);
6868
bufferService.resize(80, 30);
69-
coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService);
69+
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
7070

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

74+
describe('SL/SR/DECIC/DECDC', () => {
75+
beforeEach(() => {
76+
bufferService.resize(5, 5);
77+
optionsService.options.scrollback = 1;
78+
bufferService.reset();
79+
});
80+
it('SL (scrollLeft)', async () => {
81+
inputHandler.parseP('12345'.repeat(6));
82+
inputHandler.parseP('\x1b[ @');
83+
assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']);
84+
inputHandler.parseP('\x1b[0 @');
85+
assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']);
86+
inputHandler.parseP('\x1b[2 @');
87+
assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']);
88+
});
89+
it('SR (scrollRight)', async () => {
90+
inputHandler.parseP('12345'.repeat(6));
91+
inputHandler.parseP('\x1b[ A');
92+
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
93+
inputHandler.parseP('\x1b[0 A');
94+
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
95+
inputHandler.parseP('\x1b[2 A');
96+
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
97+
});
98+
it('insertColumns (DECIC)', async () => {
99+
inputHandler.parseP('12345'.repeat(6));
100+
inputHandler.parseP('\x1b[3;3H');
101+
inputHandler.parseP('\x1b[\'}');
102+
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
103+
bufferService.reset();
104+
inputHandler.parseP('12345'.repeat(6));
105+
inputHandler.parseP('\x1b[3;3H');
106+
inputHandler.parseP('\x1b[1\'}');
107+
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
108+
bufferService.reset();
109+
inputHandler.parseP('12345'.repeat(6));
110+
inputHandler.parseP('\x1b[3;3H');
111+
inputHandler.parseP('\x1b[2\'}');
112+
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
113+
});
114+
it('deleteColumns (DECDC)', async () => {
115+
inputHandler.parseP('12345'.repeat(6));
116+
inputHandler.parseP('\x1b[3;3H');
117+
inputHandler.parseP('\x1b[\'~');
118+
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
119+
bufferService.reset();
120+
inputHandler.parseP('12345'.repeat(6));
121+
inputHandler.parseP('\x1b[3;3H');
122+
inputHandler.parseP('\x1b[1\'~');
123+
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
124+
bufferService.reset();
125+
inputHandler.parseP('12345'.repeat(6));
126+
inputHandler.parseP('\x1b[3;3H');
127+
inputHandler.parseP('\x1b[2\'~');
128+
assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']);
129+
});
130+
});
131+
132+
describe('BS with reverseWraparound set/unset', () => {
133+
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
134+
beforeEach(() => {
135+
bufferService.resize(5, 5);
136+
optionsService.options.scrollback = 1;
137+
bufferService.reset();
138+
});
139+
describe('reverseWraparound set', () => {
140+
it('should not reverse outside of scroll margins', async () => {
141+
// prepare buffer content
142+
inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy');
143+
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
144+
assert.equal(bufferService.buffers.active.ydisp, 1);
145+
assert.equal(bufferService.buffers.active.x, 5);
146+
assert.equal(bufferService.buffers.active.y, 4);
147+
inputHandler.parseP(ttyBS.repeat(100));
148+
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
149+
150+
inputHandler.parseP('\x1b[?45h');
151+
inputHandler.parseP('uvwxy');
152+
153+
// set top/bottom to 1/3 (0-based)
154+
inputHandler.parseP('\x1b[2;4r');
155+
// place cursor below scroll bottom
156+
bufferService.buffers.active.x = 5;
157+
bufferService.buffers.active.y = 4;
158+
inputHandler.parseP(ttyBS.repeat(100));
159+
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
160+
161+
inputHandler.parseP('uvwxy');
162+
// place cursor within scroll margins
163+
bufferService.buffers.active.x = 5;
164+
bufferService.buffers.active.y = 3;
165+
inputHandler.parseP(ttyBS.repeat(100));
166+
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
167+
assert.equal(bufferService.buffers.active.x, 0);
168+
assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop
169+
170+
inputHandler.parseP('fghijklmnopqrst');
171+
// place cursor above scroll top
172+
bufferService.buffers.active.x = 5;
173+
bufferService.buffers.active.y = 0;
174+
inputHandler.parseP(ttyBS.repeat(100));
175+
assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
176+
});
177+
});
178+
});
179+
74180
it('save and restore cursor', () => {
75181
bufferService.buffer.x = 1;
76182
bufferService.buffer.y = 2;
@@ -140,7 +246,7 @@ describe('InputHandler', () => {
140246
assert.equal(coreService.decPrivateModes.bracketedPasteMode, false);
141247
});
142248
});
143-
describe('regression tests', function(): void {
249+
describe('regression tests', function (): void {
144250
function termContent(bufferService: IBufferService, trim: boolean): string[] {
145251
const result = [];
146252
for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim));
@@ -1240,7 +1346,7 @@ describe('InputHandler', () => {
12401346
await inputHandler.parseP('\x1b[6H\x1b[2Mm');
12411347
assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']);
12421348
await inputHandler.parseP('\x1b[3H\x1b[2Mn');
1243-
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
1349+
assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
12441350
});
12451351
});
12461352
it('should parse big chunks in smaller subchunks', async () => {
@@ -1820,7 +1926,7 @@ describe('InputHandler - async handlers', () => {
18201926
optionsService = new MockOptionsService();
18211927
bufferService = new BufferService(optionsService);
18221928
bufferService.resize(80, 30);
1823-
coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService);
1929+
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
18241930
coreService.onData(data => { console.log(data); });
18251931

18261932
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
@@ -1829,7 +1935,7 @@ describe('InputHandler - async handlers', () => {
18291935
it('async CUP with CPR check', async () => {
18301936
const cup: number[][] = [];
18311937
const cpr: number[][] = [];
1832-
inputHandler.registerCsiHandler({final: 'H'}, async params => {
1938+
inputHandler.registerCsiHandler({ final: 'H' }, async params => {
18331939
cup.push(params.toArray() as number[]);
18341940
await new Promise(res => setTimeout(res, 50));
18351941
// late call of real repositioning
@@ -1855,7 +1961,7 @@ describe('InputHandler - async handlers', () => {
18551961
assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']);
18561962
});
18571963
it('async DCS between', async () => {
1858-
inputHandler.registerDcsHandler({final: 'a'}, async (data, params) => {
1964+
inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => {
18591965
await new Promise(res => setTimeout(res, 50));
18601966
assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']);
18611967
assert.equal(data, 'some data');

src/common/InputHandler.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler {
240240
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
241241
private _onRequestReset = new EventEmitter<void>();
242242
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
243-
private _onRequestScroll = new EventEmitter<IAttributeData, boolean | void>();
244-
public get onRequestScroll(): IEvent<IAttributeData, boolean | void> { return this._onRequestScroll.event; }
245243
private _onRequestSyncScrollBar = new EventEmitter<void>();
246244
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
247245
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
@@ -651,7 +649,7 @@ export class InputHandler extends Disposable implements IInputHandler {
651649
buffer.y++;
652650
if (buffer.y === buffer.scrollBottom + 1) {
653651
buffer.y--;
654-
this._onRequestScroll.fire(this._eraseAttrData(), true);
652+
this._bufferService.scroll(this._eraseAttrData(), true);
655653
} else {
656654
if (buffer.y >= this._bufferService.rows) {
657655
buffer.y = this._bufferService.rows - 1;
@@ -791,7 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler {
791789
buffer.y++;
792790
if (buffer.y === buffer.scrollBottom + 1) {
793791
buffer.y--;
794-
this._onRequestScroll.fire(this._eraseAttrData());
792+
this._bufferService.scroll(this._eraseAttrData());
795793
} else if (buffer.y >= this._bufferService.rows) {
796794
buffer.y = this._bufferService.rows - 1;
797795
}
@@ -2987,7 +2985,7 @@ export class InputHandler extends Disposable implements IInputHandler {
29872985
this._bufferService.buffer.y++;
29882986
if (buffer.y === buffer.scrollBottom + 1) {
29892987
buffer.y--;
2990-
this._onRequestScroll.fire(this._eraseAttrData());
2988+
this._bufferService.scroll(this._eraseAttrData());
29912989
} else if (buffer.y >= this._bufferService.rows) {
29922990
buffer.y = this._bufferService.rows - 1;
29932991
}

0 commit comments

Comments
 (0)