Merge branch 'master' into faster_wcwidth

This commit is contained in:
Daniel Imms
2017-07-17 11:06:38 -07:00
committed by GitHub
22 changed files with 1194 additions and 533 deletions
+1
View File
@@ -9,6 +9,7 @@ before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
- npm install -g npm@5.1.0
env:
matrix:
- NPM_COMMAND=lint
+15 -2
View File
@@ -14,7 +14,7 @@ const sorcery = require('sorcery');
const source = require('vinyl-source-stream');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');
const util = require('gulp-util');
let buildDir = process.env.BUILD_DIR || 'build';
let tsProject = ts.createProject('tsconfig.json');
@@ -121,13 +121,26 @@ gulp.task('mocha', ['instrument-test'], function () {
.pipe(istanbul.writeReports());
});
/**
* Run single test file by file name(without file extension). Example of the command:
* gulp mocha-test --test InputHandler.test
*/
gulp.task('mocha-test', ['instrument-test'], function () {
let testName = util.env.test;
util.log("Run test by Name: " + testName);
return gulp.src([`${outDir}/${testName}.js`, `${outDir}/**/${testName}.js`], {read: false})
.pipe(mocha())
.once('error', () => process.exit(1))
.pipe(istanbul.writeReports());
});
/**
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
* (Without this task the source maps produced for the JavaScript bundle points into the
* compiled JavaScript files in ${outDir}/).
*/
gulp.task('sorcery', ['browserify'], function () {
var chain = sorcery.loadSync(`${buildDir}/xterm.js`);
let chain = sorcery.loadSync(`${buildDir}/xterm.js`);
chain.apply();
chain.writeSync();
});
+488 -18
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -46,13 +46,14 @@
"express-ws": "2.0.0-rc.1",
"fs-extra": "^1.0.0",
"glob": "^7.0.5",
"gulp": "^3.9.1",
"gulp": "3.9.1",
"gulp-cli": "^1.2.2",
"gulp-coveralls": "^0.1.4",
"gulp-istanbul": "^1.1.1",
"gulp-mocha": "^3.0.1",
"gulp-sourcemaps": "1.9.1",
"gulp-typescript": "^3.1.3",
"gulp-util": "3.0.8",
"jsdoc": "3.4.3",
"jsdom": "^11.1.0",
"merge-stream": "^1.0.1",
+31
View File
@@ -0,0 +1,31 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
describe('Buffer', () => {
let terminal: ITerminal;
let buffer: Buffer;
beforeEach(() => {
terminal = <any>{
cols: 80,
rows: 24,
scrollback: 1000
};
buffer = new Buffer(terminal);
});
describe('constructor', () => {
it('should create a CircularList with max length equal to scrollback, for its lines', () => {
assert.instanceOf(buffer.lines, CircularList);
assert.equal(buffer.lines.maxLength, terminal.scrollback);
});
it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => {
assert.equal(buffer.scrollBottom, terminal.rows - 1);
});
});
});
+39
View File
@@ -0,0 +1,39 @@
/**
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { CircularList } from './utils/CircularList';
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
* following information is stored (in high-level):
* - text content of this particular buffer
* - cursor position
* - scroll position
*/
export class Buffer {
public lines: CircularList<[number, string, number][]>;
/**
* Create a new Buffer.
* @param {Terminal} terminal - The terminal the Buffer will belong to
* @param {number} ydisp - The scroll position of the Buffer in the viewport
* @param {number} ybase - The scroll position of the y cursor (ybase + y = the y position within the Buffer)
* @param {number} y - The cursor's y position after ybase
* @param {number} x - The cursor's x position after ybase
*/
constructor(
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.scrollBottom = this.terminal.rows - 1;
}
}
+49
View File
@@ -0,0 +1,49 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer';
describe('BufferSet', () => {
let terminal: ITerminal;
let bufferSet: BufferSet;
beforeEach(() => {
terminal = <any>{
cols: 80,
rows: 24,
scrollback: 1000
};
bufferSet = new BufferSet(terminal);
});
describe('constructor', () => {
it('should create two different buffers: alt and normal', () => {
assert.instanceOf(bufferSet.normal, Buffer);
assert.instanceOf(bufferSet.alt, Buffer);
assert.notEqual(bufferSet.normal, bufferSet.alt);
});
});
describe('activateNormalBuffer', () => {
beforeEach(() => {
bufferSet.activateNormalBuffer();
});
it('should set the normal buffer as the currently active buffer', () => {
assert.equal(bufferSet.active, bufferSet.normal);
});
});
describe('activateAltBuffer', () => {
beforeEach(() => {
bufferSet.activateAltBuffer();
});
it('should set the alt buffer as the currently active buffer', () => {
assert.equal(bufferSet.active, bufferSet.alt);
});
});
});
+68
View File
@@ -0,0 +1,68 @@
/**
* @license MIT
*/
import { ITerminal, IBufferSet } from './Interfaces';
import { Buffer } from './Buffer';
import { EventEmitter } from './EventEmitter';
/**
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
* provides also utilities for working with them.
*/
export class BufferSet extends EventEmitter implements IBufferSet {
private _normal: Buffer;
private _alt: Buffer;
private _activeBuffer: Buffer;
/**
* Create a new BufferSet for the given terminal.
* @param {Terminal} terminal - The terminal the BufferSet will belong to
*/
constructor(private _terminal: ITerminal) {
super();
this._normal = new Buffer(this._terminal);
this._alt = new Buffer(this._terminal);
this._activeBuffer = this._normal;
}
/**
* Returns the alt Buffer of the BufferSet
* @returns {Buffer}
*/
public get alt(): Buffer {
return this._alt;
}
/**
* Returns the normal Buffer of the BufferSet
* @returns {Buffer}
*/
public get active(): Buffer {
return this._activeBuffer;
}
/**
* Returns the currently active Buffer of the BufferSet
* @returns {Buffer}
*/
public get normal(): Buffer {
return this._normal;
}
/**
* Sets the normal Buffer of the BufferSet as its currently active Buffer
*/
public activateNormalBuffer(): void {
this._activeBuffer = this._normal;
this.emit('activate', this._normal);
}
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(): void {
this._activeBuffer = this._alt;
this.emit('activate', this._alt);
}
}
+3 -1
View File
@@ -2,12 +2,14 @@
* @license MIT
*/
import { IEventEmitter } from './Interfaces';
interface ListenerType {
(): void;
listener?: () => void;
};
export class EventEmitter {
export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: ListenerType[]};
constructor() {
+169 -197
View File
File diff suppressed because it is too large Load Diff
+29 -6
View File
@@ -24,9 +24,6 @@ export interface ITerminal {
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
ybase: number;
ydisp: number;
lines: ICircularList<string>;
rows: number;
cols: number;
browser: IBrowser;
@@ -34,9 +31,10 @@ export interface ITerminal {
children: HTMLElement[];
cursorHidden: boolean;
cursorState: number;
x: number;
y: number;
defAttr: number;
scrollback: number;
buffers: IBufferSet;
buffer: IBuffer;
/**
* Emit the 'data' event and populate the given data.
@@ -48,6 +46,26 @@ export interface ITerminal {
cancel(ev: Event, force?: boolean);
log(text: string): void;
emit(event: string, data: any);
reset(): void;
showCursor(): void;
}
export interface IBuffer {
lines: ICircularList<[number, string, number][]>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
}
export interface IBufferSet {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface ISelectionManager {
@@ -71,7 +89,7 @@ export interface ILinkifier {
deregisterLinkMatcher(matcherId: number): boolean;
}
interface ICircularList<T> {
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
@@ -85,6 +103,11 @@ interface ICircularList<T> {
shiftElements(start: number, count: number, offset: number): void;
}
export interface IEventEmitter {
on(type, listener): void;
off(type, listener): void;
}
export interface LinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
+3 -3
View File
@@ -52,7 +52,7 @@ escapedStateHandler['c'] = (parser, terminal) => {
};
escapedStateHandler['E'] = (parser, terminal) => {
// ESC E Next Line ( NEL is 0x85).
terminal.x = 0;
terminal.buffer.x = 0;
terminal.index();
parser.setState(ParserState.NORMAL);
};
@@ -499,9 +499,9 @@ export class Parser {
// DECSTBM
case 'r':
pt = ''
+ (this._terminal.scrollTop + 1)
+ (this._terminal.buffer.scrollTop + 1)
+ ';'
+ (this._terminal.scrollBottom + 1)
+ (this._terminal.buffer.scrollBottom + 1)
+ 'r';
break;
+6 -6
View File
@@ -139,15 +139,15 @@ export class Renderer {
}
for (; y <= end; y++) {
let row = y + this._terminal.ydisp;
let row = y + this._terminal.buffer.ydisp;
let line = this._terminal.lines.get(row);
let line = this._terminal.buffer.lines.get(row);
let x;
if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp) &&
if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) &&
this._terminal.cursorState &&
!this._terminal.cursorHidden) {
x = this._terminal.x;
x = this._terminal.buffer.x;
} else {
x = -1;
}
@@ -337,8 +337,8 @@ export class Renderer {
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._terminal.ydisp;
const viewportEndRow = end[1] - this._terminal.ydisp;
const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
+20 -16
View File
@@ -3,16 +3,17 @@
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal, ICircularList } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: CircularList<any>,
buffer: ICircularList<[number, string, number][]>,
rowContainer: HTMLElement,
charMeasure: CharMeasure
) {
@@ -36,7 +37,7 @@ describe('SelectionManager', () => {
let document: Document;
let terminal: ITerminal;
let buffer: CircularList<any>;
let bufferLines: ICircularList<[number, string, number][]>;
let rowContainer: HTMLElement;
let selectionManager: TestSelectionManager;
@@ -44,9 +45,12 @@ describe('SelectionManager', () => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
buffer = new CircularList<any>(100);
terminal = <any>{ cols: 80, rows: 2 };
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
terminal.scrollback = 100;
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
bufferLines = terminal.buffer.lines;
selectionManager = new TestSelectionManager(terminal, bufferLines, rowContainer, null);
});
function stringToRow(text: string): [number, string, number][] {
@@ -59,7 +63,7 @@ describe('SelectionManager', () => {
describe('_selectWordAt', () => {
it('should expand selection for normal width chars', () => {
buffer.push(stringToRow('foo bar'));
bufferLines.push(stringToRow('foo bar'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([1, 0]);
@@ -76,7 +80,7 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.selectionText, 'bar');
});
it('should expand selection for whitespace', () => {
buffer.push(stringToRow('a b'));
bufferLines.push(stringToRow('a b'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'a');
selectionManager.selectWordAt([1, 0]);
@@ -90,7 +94,7 @@ describe('SelectionManager', () => {
});
it('should expand selection for wide characters', () => {
// Wide characters use a special format
buffer.push([
bufferLines.push([
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
@@ -142,7 +146,7 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.selectionText, 'foo');
});
it('should select up to non-path characters that are commonly adjacent to paths', () => {
buffer.push(stringToRow('(cd)[ef]{gh}\'ij"'));
bufferLines.push(stringToRow('(cd)[ef]{gh}\'ij"'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '(cd');
selectionManager.selectWordAt([1, 0]);
@@ -180,7 +184,7 @@ describe('SelectionManager', () => {
describe('_selectLineAt', () => {
it('should select the entire line', () => {
buffer.push(stringToRow('foo bar'));
bufferLines.push(stringToRow('foo bar'));
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
@@ -190,13 +194,13 @@ describe('SelectionManager', () => {
describe('selectAll', () => {
it('should select the entire buffer, beyond the viewport', () => {
buffer.push(stringToRow('1'));
buffer.push(stringToRow('2'));
buffer.push(stringToRow('3'));
buffer.push(stringToRow('4'));
buffer.push(stringToRow('5'));
bufferLines.push(stringToRow('1'));
bufferLines.push(stringToRow('2'));
bufferLines.push(stringToRow('3'));
bufferLines.push(stringToRow('4'));
bufferLines.push(stringToRow('5'));
selectionManager.selectAll();
terminal.ybase = buffer.length - terminal.rows;
terminal.buffer.ybase = bufferLines.length - terminal.rows;
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
});
});
+8 -8
View File
@@ -7,7 +7,7 @@ import * as Browser from './utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { ITerminal } from './Interfaces';
import { ITerminal, ICircularList } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import { translateBufferLineToString } from './utils/BufferLine';
@@ -98,7 +98,7 @@ export class SelectionManager extends EventEmitter {
constructor(
private _terminal: ITerminal,
private _buffer: CircularList<any>,
private _buffer: ICircularList<[number, string, number][]>,
private _rowContainer: HTMLElement,
private _charMeasure: CharMeasure
) {
@@ -147,7 +147,7 @@ export class SelectionManager extends EventEmitter {
* switched in or out.
* @param buffer The active buffer.
*/
public setBuffer(buffer: CircularList<any>): void {
public setBuffer(buffer: ICircularList<[number, string, number][]>): void {
this._buffer = buffer;
this.clearSelection();
}
@@ -186,7 +186,7 @@ export class SelectionManager extends EventEmitter {
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.get(i);
const lineText = translateBufferLineToString(bufferLine, true);
if (bufferLine.isWrapped) {
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -197,7 +197,7 @@ export class SelectionManager extends EventEmitter {
if (start[1] !== end[1]) {
const bufferLine = this._buffer.get(end[1]);
const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]);
if (bufferLine.isWrapped) {
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -281,7 +281,7 @@ export class SelectionManager extends EventEmitter {
coords[0]--;
coords[1]--;
// Convert viewport coords to buffer coords
coords[1] += this._terminal.ydisp;
coords[1] += this._terminal.buffer.ydisp;
return coords;
}
@@ -476,9 +476,9 @@ export class SelectionManager extends EventEmitter {
this._terminal.scrollDisp(this._dragScrollAmount, false);
// Re-evaluate selection
if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.ydisp + this._terminal.rows];
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
} else {
this._model.selectionEnd = [0, this._terminal.ydisp];
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
}
this.refresh();
}
+5
View File
@@ -4,6 +4,7 @@
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import {BufferSet} from './BufferSet';
class TestSelectionModel extends SelectionModel {
constructor(
@@ -22,6 +23,10 @@ describe('SelectionManager', () => {
beforeEach(() => {
terminal = <any>{ cols: 80, rows: 2, ybase: 0 };
terminal.scrollback = 10;
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
model = new TestSelectionModel(terminal);
});
+1 -1
View File
@@ -68,7 +68,7 @@ export class SelectionModel {
*/
public get finalSelectionEnd(): [number, number] {
if (this.isSelectAllActive) {
return [this._terminal.cols, this._terminal.ybase + this._terminal.rows - 1];
return [this._terminal.cols, this._terminal.buffer.ybase + this._terminal.rows - 1];
}
if (!this.selectionStart) {
+11 -9
View File
@@ -1,10 +1,10 @@
import { assert } from 'chai';
import { Viewport } from './Viewport';
import {BufferSet} from './BufferSet';
describe('Viewport', () => {
let terminal;
let viewportElement;
let selectionContainer;
let charMeasure;
let viewport;
let scrollAreaElement;
@@ -13,7 +13,6 @@ describe('Viewport', () => {
beforeEach(() => {
terminal = {
lines: [],
rows: 0,
ydisp: 0,
on: () => {},
@@ -26,8 +25,11 @@ describe('Viewport', () => {
style: {
height: 0
}
}
},
scrollback: 10
};
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
viewportElement = {
addEventListener: () => {},
style: {
@@ -60,14 +62,14 @@ describe('Viewport', () => {
}, 0);
});
it('should set the height of the viewport when the line-height changed', () => {
terminal.lines.push('');
terminal.lines.push('');
terminal.buffer.lines.push('');
terminal.buffer.lines.push('');
terminal.rows = 1;
viewport.refresh();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
charMeasure.height = 20;
charMeasure.height = 2 * CHARACTER_HEIGHT;
viewport.refresh();
assert.equal(viewportElement.style.height, 20 + 'px');
assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
});
});
@@ -75,13 +77,13 @@ describe('Viewport', () => {
it('should sync the scroll area', done => {
// Allow CharMeasure to be initialized
setTimeout(() => {
terminal.lines.push('');
terminal.buffer.lines.push('');
terminal.rows = 1;
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
terminal.lines.push('');
terminal.buffer.lines.push('');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
+5 -7
View File
@@ -20,7 +20,7 @@ export class Viewport {
* @param terminal The terminal this viewport belongs to.
* @param viewportElement The DOM element acting as the viewport.
* @param scrollArea The DOM element acting as the scroll area.
* @param charMeasureElement A DOM element used to measure the character size of. the terminal.
* @param charMeasure A DOM element used to measure the character size of. the terminal.
*/
constructor(
private terminal: ITerminal,
@@ -43,8 +43,6 @@ export class Viewport {
/**
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
* be created.
*/
private refresh(): void {
if (this.charMeasure.height > 0) {
@@ -68,9 +66,9 @@ export class Viewport {
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
// If buffer height changed
this.lastRecordedBufferLength = this.terminal.lines.length;
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
this.refresh();
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
// If viewport height changed
@@ -83,7 +81,7 @@ export class Viewport {
}
// Sync scrollTop
const scrollTop = this.terminal.ydisp * this.currentRowHeight;
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
if (this.viewportElement.scrollTop !== scrollTop) {
this.viewportElement.scrollTop = scrollTop;
}
@@ -96,7 +94,7 @@ export class Viewport {
*/
private onScroll(ev: Event) {
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.ydisp;
const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollDisp(diff, true);
}
+2 -2
View File
@@ -62,10 +62,10 @@ function formatError(in_, out_, expected) {
function terminalToString(term) {
var result = '';
var line_s = '';
for (var line = term.ybase; line < term.ybase + term.rows; line++) {
for (var line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
line_s = '';
for (var cell=0; cell<term.cols; ++cell) {
line_s += term.lines.get(line)[cell][1];
line_s += term.buffer.lines.get(line)[cell][1];
}
// rtrim empty cells as xterm does
line_s = line_s.replace(/\s+$/, '');

Some files were not shown because too many files have changed in this diff Show More