Clean up buffer clean up/fill logic

The alt buffer is now cleared immediated after activating the normal buffer and
is filled when switching to it. The tests were failing because the alt buffer
wasn't being cleared properly with the previous solution.
This commit is contained in:
Daniel Imms 2017-08-05 19:42:52 -07:00
parent 3d20c2f266
commit f03d00a497
10 changed files with 139 additions and 52 deletions

View File

@ -2,7 +2,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal } from './Interfaces'; import { ITerminal, IBuffer } from './Interfaces';
import { CircularList } from './utils/CircularList'; import { CircularList } from './utils/CircularList';
/** /**
@ -12,9 +12,16 @@ import { CircularList } from './utils/CircularList';
* - cursor position * - cursor position
* - scroll position * - scroll position
*/ */
export class Buffer { export class Buffer implements IBuffer {
public readonly lines: CircularList<[number, string, number][]>; private _lines: CircularList<[number, string, number][]>;
public ydisp: number;
public ybase: number;
public y: number;
public x: number;
public scrollBottom: number;
public scrollTop: number;
public tabs: any;
public savedY: number; public savedY: number;
public savedX: number; public savedX: number;
@ -27,34 +34,51 @@ export class Buffer {
* @param {number} x - The cursor's x position after ybase * @param {number} x - The cursor's x position after ybase
*/ */
constructor( constructor(
private _terminal: ITerminal, private _terminal: ITerminal
public ydisp: number = 0,
public ybase: number = 0,
public y: number = 0,
public x: number = 0,
public scrollBottom: number = 0,
public scrollTop: number = 0,
public tabs: any = {},
) { ) {
this.lines = new CircularList<[number, string, number][]>(this._terminal.scrollback); this.clear();
}
public get lines(): CircularList<[number, string, number][]> {
return this._lines;
}
public fillViewportRows(): void {
if (this._lines.length === 0) {
let i = this._terminal.rows;
while (i--) {
this.lines.push(this._terminal.blankLine());
}
}
}
public clear(): void {
this.ydisp = 0;
this.ybase = 0;
this.y = 0;
this.x = 0;
this.scrollBottom = 0;
this.scrollTop = 0;
this.tabs = {};
this._lines = new CircularList<[number, string, number][]>(this._terminal.scrollback);
this.scrollBottom = this._terminal.rows - 1; this.scrollBottom = this._terminal.rows - 1;
} }
public resize(newCols: number, newRows: number): void { public resize(newCols: number, newRows: number): void {
// Don't resize the buffer if it's empty and hasn't been used yet. // Don't resize the buffer if it's empty and hasn't been used yet.
if (this.lines.length === 0) { if (this._lines.length === 0) {
return; return;
} }
// Deal with columns increasing (we don't do anything when columns reduce) // Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) { if (this._terminal.cols < newCols) {
const ch: [number, string, number] = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr? const ch: [number, string, number] = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr?
for (let i = 0; i < this.lines.length; i++) { for (let i = 0; i < this._lines.length; i++) {
if (this.lines.get(i) === undefined) { if (this._lines.get(i) === undefined) {
this.lines.set(i, this._terminal.blankLine()); this._lines.set(i, this._terminal.blankLine());
} }
while (this.lines.get(i).length < newCols) { while (this._lines.get(i).length < newCols) {
this.lines.get(i).push(ch); this._lines.get(i).push(ch);
} }
} }
} }
@ -63,8 +87,8 @@ export class Buffer {
let addToY = 0; let addToY = 0;
if (this._terminal.rows < newRows) { if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) { for (let y = this._terminal.rows; y < newRows; y++) {
if (this.lines.length < newRows + this.ybase) { if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line, // There is room above the buffer and there are no empty elements below the line,
// scroll up // scroll up
this.ybase--; this.ybase--;
@ -76,16 +100,16 @@ export class Buffer {
} else { } else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there // Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor // are blank lines after the cursor
this.lines.push(this._terminal.blankLine()); this._lines.push(this._terminal.blankLine());
} }
} }
} }
} else { // (this._terminal.rows >= newRows) } else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) { for (let y = this._terminal.rows; y > newRows; y--) {
if (this.lines.length > newRows + this.ybase) { if (this._lines.length > newRows + this.ybase) {
if (this.lines.length > this.ybase + this.y + 1) { if (this._lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it // The line is a blank line below the cursor, remove it
this.lines.pop(); this._lines.pop();
} else { } else {
// The line is the cursor, scroll down // The line is the cursor, scroll down
this.ybase++; this.ybase++;

View File

@ -5,17 +5,17 @@ import { assert } from 'chai';
import { ITerminal } from './Interfaces'; import { ITerminal } from './Interfaces';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer'; import { Buffer } from './Buffer';
import { MockTerminal } from './utils/TestUtils';
describe('BufferSet', () => { describe('BufferSet', () => {
let terminal: ITerminal; let terminal: ITerminal;
let bufferSet: BufferSet; let bufferSet: BufferSet;
beforeEach(() => { beforeEach(() => {
terminal = <any>{ terminal = new MockTerminal();
cols: 80, terminal.cols = 80;
rows: 24, terminal.rows = 24;
scrollback: 1000 terminal.scrollback = 1000;
};
bufferSet = new BufferSet(terminal); bufferSet = new BufferSet(terminal);
}); });

View File

@ -22,6 +22,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
constructor(private _terminal: ITerminal) { constructor(private _terminal: ITerminal) {
super(); super();
this._normal = new Buffer(this._terminal); this._normal = new Buffer(this._terminal);
this._normal.fillViewportRows();
this._alt = new Buffer(this._terminal); this._alt = new Buffer(this._terminal);
this._activeBuffer = this._normal; this._activeBuffer = this._normal;
} }
@ -54,6 +55,11 @@ export class BufferSet extends EventEmitter implements IBufferSet {
* Sets the normal Buffer of the BufferSet as its currently active Buffer * Sets the normal Buffer of the BufferSet as its currently active Buffer
*/ */
public activateNormalBuffer(): void { public activateNormalBuffer(): void {
// The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new
// when activated.
this._alt.clear();
this._activeBuffer = this._normal; this._activeBuffer = this._normal;
this.emit('activate', this._normal); this.emit('activate', this._normal);
} }
@ -62,6 +68,10 @@ export class BufferSet extends EventEmitter implements IBufferSet {
* Sets the alt Buffer of the BufferSet as its currently active Buffer * Sets the alt Buffer of the BufferSet as its currently active Buffer
*/ */
public activateAltBuffer(): void { public activateAltBuffer(): void {
// Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it.
this._alt.fillViewportRows();
this._activeBuffer = this._alt; this._activeBuffer = this._alt;
this.emit('activate', this._alt); this.emit('activate', this._alt);
} }

View File

@ -954,7 +954,6 @@ export class InputHandler implements IInputHandler {
case 47: // alt screen buffer case 47: // alt screen buffer
case 1047: // alt screen buffer case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer(); this._terminal.buffers.activateAltBuffer();
this._terminal.reset();
this._terminal.viewport.syncScrollArea(); this._terminal.viewport.syncScrollArea();
this._terminal.showCursor(); this._terminal.showCursor();
break; break;

View File

@ -93,8 +93,8 @@ export interface ILinkifier {
export interface ICircularList<T> extends IEventEmitter { export interface ICircularList<T> extends IEventEmitter {
length: number; length: number;
maxLength: number; maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
forEach(callbackfn: (value: T, index: number, array: T[]) => void): void;
get(index: number): T; get(index: number): T;
set(index: number, value: T): void; set(index: number, value: T): void;
push(value: T): void; push(value: T): void;

View File

@ -9,6 +9,7 @@ import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager'; import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { MockTerminal } from './utils/TestUtils';
class TestSelectionManager extends SelectionManager { class TestSelectionManager extends SelectionManager {
constructor( constructor(
@ -46,7 +47,9 @@ describe('SelectionManager', () => {
window = dom.window; window = dom.window;
document = window.document; document = window.document;
rowContainer = document.createElement('div'); rowContainer = document.createElement('div');
terminal = <any>{ cols: 80, rows: 2 }; terminal = new MockTerminal();
terminal.cols = 80;
terminal.rows = 2;
terminal.scrollback = 100; terminal.scrollback = 100;
terminal.buffers = new BufferSet(terminal); terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active; terminal.buffer = terminal.buffers.active;
@ -64,7 +67,7 @@ describe('SelectionManager', () => {
describe('_selectWordAt', () => { describe('_selectWordAt', () => {
it('should expand selection for normal width chars', () => { it('should expand selection for normal width chars', () => {
bufferLines.push(stringToRow('foo bar')); bufferLines.set(0, stringToRow('foo bar'));
selectionManager.selectWordAt([0, 0]); selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'foo'); assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([1, 0]); selectionManager.selectWordAt([1, 0]);
@ -81,7 +84,7 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.selectionText, 'bar'); assert.equal(selectionManager.selectionText, 'bar');
}); });
it('should expand selection for whitespace', () => { it('should expand selection for whitespace', () => {
bufferLines.push(stringToRow('a b')); bufferLines.set(0, stringToRow('a b'));
selectionManager.selectWordAt([0, 0]); selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'a'); assert.equal(selectionManager.selectionText, 'a');
selectionManager.selectWordAt([1, 0]); selectionManager.selectWordAt([1, 0]);
@ -95,7 +98,7 @@ describe('SelectionManager', () => {
}); });
it('should expand selection for wide characters', () => { it('should expand selection for wide characters', () => {
// Wide characters use a special format // Wide characters use a special format
bufferLines.push([ bufferLines.set(0, [
[null, '中', 2], [null, '中', 2],
[null, '', 0], [null, '', 0],
[null, '文', 2], [null, '文', 2],
@ -147,7 +150,7 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.selectionText, 'foo'); assert.equal(selectionManager.selectionText, 'foo');
}); });
it('should select up to non-path characters that are commonly adjacent to paths', () => { it('should select up to non-path characters that are commonly adjacent to paths', () => {
bufferLines.push(stringToRow('(cd)[ef]{gh}\'ij"')); bufferLines.set(0, stringToRow('(cd)[ef]{gh}\'ij"'));
selectionManager.selectWordAt([0, 0]); selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '(cd'); assert.equal(selectionManager.selectionText, '(cd');
selectionManager.selectWordAt([1, 0]); selectionManager.selectWordAt([1, 0]);
@ -185,7 +188,7 @@ describe('SelectionManager', () => {
describe('_selectLineAt', () => { describe('_selectLineAt', () => {
it('should select the entire line', () => { it('should select the entire line', () => {
bufferLines.push(stringToRow('foo bar')); bufferLines.set(0, stringToRow('foo bar'));
selectionManager.selectLineAt(0); selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct'); assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
@ -195,11 +198,12 @@ describe('SelectionManager', () => {
describe('selectAll', () => { describe('selectAll', () => {
it('should select the entire buffer, beyond the viewport', () => { it('should select the entire buffer, beyond the viewport', () => {
bufferLines.push(stringToRow('1')); bufferLines.length = 5;
bufferLines.push(stringToRow('2')); bufferLines.set(0, stringToRow('1'));
bufferLines.push(stringToRow('3')); bufferLines.set(1, stringToRow('2'));
bufferLines.push(stringToRow('4')); bufferLines.set(2, stringToRow('3'));
bufferLines.push(stringToRow('5')); bufferLines.set(3, stringToRow('4'));
bufferLines.set(4, stringToRow('5'));
selectionManager.selectAll(); selectionManager.selectAll();
terminal.buffer.ybase = bufferLines.length - terminal.rows; terminal.buffer.ybase = bufferLines.length - terminal.rows;
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');

View File

@ -5,6 +5,7 @@ import { assert } from 'chai';
import { ITerminal } from './Interfaces'; import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import {BufferSet} from './BufferSet'; import {BufferSet} from './BufferSet';
import { MockTerminal } from './utils/TestUtils';
class TestSelectionModel extends SelectionModel { class TestSelectionModel extends SelectionModel {
constructor( constructor(
@ -22,7 +23,9 @@ describe('SelectionManager', () => {
let model: TestSelectionModel; let model: TestSelectionModel;
beforeEach(() => { beforeEach(() => {
terminal = <any>{ cols: 80, rows: 2, ybase: 0 }; terminal = new MockTerminal();
terminal.cols = 80;
terminal.rows = 2;
terminal.scrollback = 10; terminal.scrollback = 10;
terminal.buffers = new BufferSet(terminal); terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active; terminal.buffer = terminal.buffers.active;

View File

@ -5,8 +5,9 @@
* @license MIT * @license MIT
*/ */
import { EventEmitter } from '../EventEmitter'; import { EventEmitter } from '../EventEmitter';
import { ICircularList } from '../Interfaces';
export class CircularList<T> extends EventEmitter { export class CircularList<T> extends EventEmitter implements ICircularList<T> {
private _array: T[]; private _array: T[];
private _startIndex: number; private _startIndex: number;
private _length: number; private _length: number;

53
src/utils/TestUtils.ts Normal file
View File

@ -0,0 +1,53 @@
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager } from '../Interfaces';
export class MockTerminal implements ITerminal {
public element: HTMLElement;
public rowContainer: HTMLElement;
public selectionContainer: HTMLElement;
public selectionManager: ISelectionManager;
public charMeasure: ICharMeasure;
public textarea: HTMLTextAreaElement;
public rows: number;
public cols: number;
public browser: IBrowser;
public writeBuffer: string[];
public children: HTMLElement[];
public cursorHidden: boolean;
public cursorState: number;
public defAttr: number;
public scrollback: number;
public buffers: IBufferSet;
public buffer: IBuffer;
handler(data: string) {
throw new Error('Method not implemented.');
}
on(event: string, callback: () => void) {
throw new Error('Method not implemented.');
}
scrollDisp(disp: number, suppressScrollEvent: boolean) {
throw new Error('Method not implemented.');
}
cancel(ev: Event, force?: boolean) {
throw new Error('Method not implemented.');
}
log(text: string): void {
throw new Error('Method not implemented.');
}
emit(event: string, data: any) {
throw new Error('Method not implemented.');
}
reset(): void {
throw new Error('Method not implemented.');
}
showCursor(): void {
throw new Error('Method not implemented.');
}
blankLine(cur?: boolean, isWrapped?: boolean) {
const line = [];
for (let i = 0; i < this.cols; i++) {
line.push([0, ' ', 1]);
}
return line;
}
}

View File

@ -221,17 +221,12 @@ function Terminal(options) {
this.surrogate_high = ''; this.surrogate_high = '';
// Create the terminal's buffers and set the current buffer // Create the terminal's buffers and set the current buffer
this.buffers = this.buffers || new BufferSet(this); this.buffers = new BufferSet(this);
this.buffer = this.buffers.active; // Convenience shortcut; this.buffer = this.buffers.active; // Convenience shortcut;
this.buffers.on('activate', function (buffer) { this.buffers.on('activate', function (buffer) {
this._terminal.buffer = buffer; this._terminal.buffer = buffer;
}); });
var i = this.rows;
while (i--) {
this.buffer.lines.push(this.blankLine());
}
// Ensure the selection manager has the correct buffer // Ensure the selection manager has the correct buffer
if (this.selectionManager) { if (this.selectionManager) {
this.selectionManager.setBuffer(this.buffer.lines); this.selectionManager.setBuffer(this.buffer.lines);
@ -2228,12 +2223,10 @@ Terminal.prototype.reset = function() {
var customKeyEventHandler = this.customKeyEventHandler; var customKeyEventHandler = this.customKeyEventHandler;
var cursorBlinkInterval = this.cursorBlinkInterval; var cursorBlinkInterval = this.cursorBlinkInterval;
var inputHandler = this.inputHandler; var inputHandler = this.inputHandler;
var buffers = this.buffers;
Terminal.call(this, this.options); Terminal.call(this, this.options);
this.customKeyEventHandler = customKeyEventHandler; this.customKeyEventHandler = customKeyEventHandler;
this.cursorBlinkInterval = cursorBlinkInterval; this.cursorBlinkInterval = cursorBlinkInterval;
this.inputHandler = inputHandler; this.inputHandler = inputHandler;
this.buffers = buffers;
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea(); this.viewport.syncScrollArea();
}; };