Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logic for reflowing lines #644

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class InputHandler implements IInputHandler {
if (this._terminal.x + ch_width - 1 >= this._terminal.cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
// Mark this row as being wrapped.
this._terminal.lines.addWrappedLine(row);
this._terminal.x = 0;
this._terminal.y++;
if (this._terminal.y > this._terminal.scrollBottom) {
Expand All @@ -79,7 +81,7 @@ export class InputHandler implements IInputHandler {
this._terminal.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];

// insert empty cell at cursor
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, ' ', 1]);
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, null, 1]);
}
}

Expand Down Expand Up @@ -152,7 +154,12 @@ export class InputHandler implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I).
*/
public tab(): void {
this._terminal.x = this._terminal.nextStop();
const stop = this._terminal.nextStop();
let row = this._terminal.lines.get(this._terminal.y + this._terminal.ybase);
while (this._terminal.x <= stop) {
row[this._terminal.x] = [this._terminal.defAttr, ' ', 1];
this._terminal.x++;
}
}

/**
Expand Down Expand Up @@ -504,7 +511,7 @@ export class InputHandler implements IInputHandler {
}

row = this._terminal.y + this._terminal.ybase;
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
ch = [this._terminal.eraseAttr(), null, 1]; // xterm

while (param--) {
this._terminal.lines.get(row).splice(this._terminal.x, 1);
Expand Down Expand Up @@ -554,7 +561,7 @@ export class InputHandler implements IInputHandler {

row = this._terminal.y + this._terminal.ybase;
j = this._terminal.x;
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
ch = [this._terminal.eraseAttr(), null, 1]; // xterm

while (param-- && j < this._terminal.cols) {
this._terminal.lines.get(row)[j++] = ch;
Expand Down Expand Up @@ -608,7 +615,7 @@ export class InputHandler implements IInputHandler {
public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1
, line = this._terminal.lines.get(this._terminal.ybase + this._terminal.y)
, ch = line[this._terminal.x - 1] || [this._terminal.defAttr, ' ', 1];
, ch = line[this._terminal.x - 1] || [this._terminal.defAttr, null, 1];

while (param--) {
line[this._terminal.x++] = ch;
Expand Down Expand Up @@ -1131,6 +1138,10 @@ export class InputHandler implements IInputHandler {
// this.x = this.savedX;
// this.y = this.savedY;
// }

// Run the resize handler in case the viewport has been resized since we switched buffers
this._terminal.resize(this._terminal.cols, this._terminal.rows, true);

this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
Expand Down
10 changes: 9 additions & 1 deletion src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface ITerminal {
textarea: HTMLTextAreaElement;
ybase: number;
ydisp: number;
lines: ICircularList<string>;
lines: IWrappableList;
rows: number;
cols: number;
browser: IBrowser;
Expand Down Expand Up @@ -74,6 +74,14 @@ interface ICircularList<T> {
shiftElements(start: number, count: number, offset: number): void;
}

export interface IWrappableList extends ICircularList<any[]> {
/**
* Reflows lines in this list to a new maxwidth.
*/
wrappedLines: number[];
reflow(width: number, oldWidth: number): IWrappableList;
}

export interface LinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
Expand Down
6 changes: 6 additions & 0 deletions src/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ export class Renderer {
}

this._terminal.children[y].innerHTML = out;

if (this._terminal.lines.wrappedLines.indexOf(row) > -1) {
this._terminal.children[y].style.background = 'red';
} else {
this._terminal.children[y].style.background = 'black';
}
}

if (parent) {
Expand Down
4 changes: 4 additions & 0 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export type LinkMatcher = {
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;

export type CharData = [number, string | null, number];

export type RowData = CharData[];
4 changes: 3 additions & 1 deletion src/test/escape-sequences-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ function formatError(in_, out_, expected) {
function terminalToString(term) {
var result = '';
var line_s = '';
var ch;
for (var line = term.ybase; line < term.ybase + term.rows; line++) {
line_s = '';
for (var cell=0; cell<term.cols; ++cell) {
line_s += term.lines.get(line)[cell][1];
ch = term.lines.get(line)[cell][1];
line_s += (ch === null ? ' ' : ch);
}
// rtrim empty cells as xterm does
line_s = line_s.replace(/\s+$/, '');
Expand Down
16 changes: 10 additions & 6 deletions src/utils/CircularList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* @license MIT
*/
export class CircularList<T> {
private _array: T[];
private _startIndex: number;
private _length: number;
protected _array: T[];
protected _startIndex: number;
protected _length: number;

constructor(maxLength: number) {
this._array = new Array<T>(maxLength);
Expand Down Expand Up @@ -43,8 +43,12 @@ export class CircularList<T> {
this._length = newLength;
}

public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void {
return this._array.forEach;
public forEach(callbackfn: (value: T, index?: number, array?: T[]) => void): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this need to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stood, the forEach implementation didn't take into account the cyclic index, so as soon as the list started to wrap, the behaviour became unpredictable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, this may be causing other obscure bugs if it's used anywhere important.

If this is the case though, I think the new forEach needs to take into account the _startIndex since a buffer can start from any index and not necessarily wrap all the way around. https://github.com/sourcelair/xterm.js/blob/944da2808906aa86b6ad096fd0b179e8cf87e0b8/src/utils/CircularList.ts#L140

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the get method uses _getCyclicIndex which uses _startIndex though?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 😄

Is there any reason you use .bind? This has a big downside in TypeScript as you lose typing information.

Also I think you should use .length instead of .maxLength for the reason mentioned above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on using .length! I think the .bind is left over from earlier experiments and can be removed.

let i = 0;
let len = this.length;
for (i; i < len; i++) {
callbackfn(this.get(i), i);
}
}

/**
Expand Down Expand Up @@ -177,7 +181,7 @@ export class CircularList<T> {
* @param index The regular index.
* @returns The cyclic index.
*/
private _getCyclicIndex(index: number): number {
protected _getCyclicIndex(index: number): number {
return (this._startIndex + index) % this.maxLength;
}
}
150 changes: 150 additions & 0 deletions src/utils/WrappableList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { CircularList } from './CircularList';
import { RowData } from '../Types';

import { IWrappableList } from '../Interfaces';

function fastForeach(array, fn) {
let i = 0;
let len = array.length;
for (i; i < len; i++) {
fn(array[i], i, array);
}
}

function trimmedLength(line) {
let i = 0;
let len = line.length;
for (i; i < len; i++) {
if (!line[i] || (line[i] && line[i][1] === null)) {
break;
}
}

return i;
}

export class WrappableList extends CircularList<RowData> {
private _wrappedLineIncrement: number[] = [];
private _blankline: RowData;
public wrappedLines: number[] = [];

constructor(maxLength: number, private _terminal) {
super(maxLength);

this._blankline = this._terminal.blankLine();
}

public push(value: RowData): void {
// Need to make sure wrappedlines move when CircularList wraps around, but without increasing
// the time complexity of `push`. We push the number of `wrappedLines` that should be
// incremented so that it can be calculated later.
if (this._length + 1 === this.maxLength) {
this._wrappedLineIncrement.push(this.wrappedLines.length);
}
super.push(value);
}

public addWrappedLine(row: number): void {
this.wrappedLines.push(row);
}

// Adjusts `wrappedLines` using `_wrappedLineIncrement`
private _adjustWrappedLines(): void {
fastForeach(this._wrappedLineIncrement, (end) => {
let i = 0;
for (i; i < end; i++) {
this.wrappedLines[i] -= 1;
}
});
this._wrappedLineIncrement = [];
}

private _numArrayToObject(array: number[]) {
let i = 0;
let len = array.length;
let returnObject = {};
for (i; i < len; i++) {
returnObject[array[i]] = null;
}
return returnObject;
}

/**
* Reflow lines to a new max width.
* A record of which lines are wrapped is stored in `wrappedLines` and is used to join and split
* lines correctly.
*/
public reflow(width: number, oldWidth: number) {
const temp = [];
const tempWrapped = [];
const wrappedLines = this.wrappedLines;
let masterIndex = 0;
let len = this.length;
let line;
let trim;
let isWidthDecreasing = width < oldWidth;
let i = 0;
let xj;

this._adjustWrappedLines();
// Using in index accessor is much quicker when we need to calculate previouslyWrapped many times
const wrappedLinesObject = this._numArrayToObject(this.wrappedLines);

const concatWrapped = (data, index) => {
let next = index;
while (wrappedLinesObject[next] !== undefined) {
next++;
masterIndex++;
Array.prototype.push.apply(data, this.get(next));
}
return data;
};

// A for loop is used here so that masterIndex can be advanced when concatting lines in
// the 'concatWrapped' method
for (masterIndex; masterIndex < len; masterIndex++) {
line = concatWrapped(this.get(masterIndex), masterIndex);
trim = trimmedLength(line);

if (trim > width) {
line.length = trim;
xj = fastCeil(trim / width) - 1;
for (i = 0; i < trim; i += width) {
if (width > line.length) {
temp.push(line);
} else {
temp.push(line.splice(0, width));
}

if (xj-- > 0) {
tempWrapped.push(temp.length - 1);
}
}
} else {
if (isWidthDecreasing) {
line.length = width;
}
temp.push(line);
}
}

// Chop the reflow list to length and push it into a new CircularList, also compensate wrapped
// lines for new start point of list
const scrollback = this.maxLength;
let pushStart = temp.length > scrollback ?
temp.length - scrollback :
0;
if (pushStart > 0) {
for (i = 0; i < tempWrapped.length; i++) {
tempWrapped[i] -= pushStart;
}
}
let newList = new WrappableList(scrollback, this._terminal);
for (i = pushStart; i < temp.length; i++) {
newList.push(temp[i]);
}
newList.wrappedLines = tempWrapped;

return newList;
}
}
Loading