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

Reflow text on resize #1864

Merged
merged 38 commits into from
Jan 25, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8a50729
Reflow wider
Tyriar Dec 28, 2018
314f98f
Mostly working for reflowing to smaller
Tyriar Dec 28, 2018
fa47036
Fix row removal in reflowLarger
Tyriar Dec 28, 2018
8bc04c2
Tidy up
Tyriar Dec 28, 2018
3311ed5
Do shrink in reverse, fix up row remove count again
Tyriar Dec 28, 2018
ae29dbb
Fix scrollbar when wrapping beyond single viewport of data
Tyriar Dec 28, 2018
7684f93
Fix ydisp/ybase after trimming buffer
Tyriar Dec 28, 2018
358898d
Fix some tests
Tyriar Dec 28, 2018
a33e8a6
Properly shrink rows to cols every time
Tyriar Dec 28, 2018
2a0da17
Add a bunch of reflow tests
Tyriar Dec 28, 2018
72369e0
Only enable reflow on the normal buffer
Tyriar Dec 28, 2018
7435971
Merge remote-tracking branch 'origin/master' into 622_reflow3
Tyriar Dec 29, 2018
4a9f10d
Remove some of Buffer's dependency on Terminal
Tyriar Dec 29, 2018
dde9618
Keep track of cols/rows inside Buffer
Tyriar Dec 29, 2018
478742a
Make reflow small crazy fast
Tyriar Dec 30, 2018
c9f4a65
Clean up comments and todos
Tyriar Dec 31, 2018
b7081ab
Move loop into reflowLarger (adjust indent)
Tyriar Dec 31, 2018
135e31f
Speed up reflow larger by batching removals
Tyriar Dec 31, 2018
1612cec
Fix reflow larger bug, add regression test
Tyriar Dec 31, 2018
db488eb
Reflow combined chars
Tyriar Dec 31, 2018
40e8618
Discard cut off combined data when resizing BufferLines
Tyriar Dec 31, 2018
840970e
Update markers after a reflow
Tyriar Dec 31, 2018
6559931
Add lots of tests
Tyriar Dec 31, 2018
2ce67b8
Remove unneeded MockTerminal member
Tyriar Dec 31, 2018
90950e2
Merge branch 'master' into 622_reflow3
jerch Jan 5, 2019
6581eb7
fix leftover BufferLineConstructor
jerch Jan 5, 2019
7fe3f0a
Improve BufferLine test
Tyriar Jan 11, 2019
68197ed
Merge branch 'master' into 622_reflow3
Tyriar Jan 17, 2019
4843ca5
Fix reflow larger with wide chars
Tyriar Jan 21, 2019
ce69dce
Progress on reflow smaller with wide chars
Tyriar Jan 24, 2019
df7cd9c
Get reflow smaller working for wide chars
Tyriar Jan 24, 2019
178c513
Clean up
Tyriar Jan 24, 2019
deb04ca
Merge branch 'master' into 622_reflow3
Tyriar Jan 24, 2019
dfff04c
Pull toRemove step into BufferReflow
Tyriar Jan 24, 2019
d4bd8ae
Pull more parts out of reflow larger
Tyriar Jan 24, 2019
99ac780
Remove out param from reflow large method
Tyriar Jan 24, 2019
109e3a5
jsdoc
Tyriar Jan 24, 2019
52a429f
Merge branch 'master' into 622_reflow3
Tyriar Jan 25, 2019
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
178 changes: 178 additions & 0 deletions src/Buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,184 @@ export class Buffer implements IBuffer {
}

this.scrollBottom = newRows - 1;

if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') {
this._reflow(newCols);
}
}

private _reflow(newCols: number): void {
if (this._terminal.cols === newCols) {
return;
}

// Iterate through rows, ignore the last one as it cannot be wrapped
if (newCols > this._terminal.cols) {
for (let y = 0; y < this.lines.length - 1; y++) {
y += this._reflowLarger(y, newCols);
}
} else {
// Go backwards as many lines may be trimmed and this will avoid considering them
for (let y = this.lines.length - 1; y >= 0; y--) {
y -= this._reflowSmaller(y, newCols);
}
}
}

private _reflowLarger(y: number, newCols: number): number {
// Check if this row is wrapped
let i = y;
let nextLine = this.lines.get(++i) as BufferLine;
if (!nextLine.isWrapped) {
return 0;
}

// Check how many lines it's wrapped for
const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine];
while (nextLine.isWrapped) {
wrappedLines.push(nextLine);
nextLine = this.lines.get(++i) as BufferLine;
}

// Copy buffer data to new locations
let destLineIndex = 0;
let destCol = this._terminal.cols;
let srcLineIndex = 1;
let srcCol = 0;
while (srcLineIndex < wrappedLines.length) {
const srcRemainingCells = this._terminal.cols - srcCol;
const destRemainingCells = newCols - destCol;
const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);
destCol += cellsToCopy;
if (destCol === newCols) {
destLineIndex++;
destCol = 0;
}
srcCol += cellsToCopy;
if (srcCol === this._terminal.cols) {
srcLineIndex++;
srcCol = 0;
}
}

// Clear out remaining cells or fragments could remain
// TODO: @jerch can fillCharData be a const?
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData);

// Work backwards and remove any rows at the end that only contain null cells
let countToRemove = 0;
for (let i = wrappedLines.length - 1; i > 0; i--) {
if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {
countToRemove++;
} else {
break;
}
}

if (countToRemove > 0) {
this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove);
let viewportAdjustments = countToRemove;
while (viewportAdjustments-- > 0) {
if (this.ybase === 0) {
this.y--;
// Add an extra row at the bottom of the viewport
this.lines.push(new this._bufferLineConstructor(newCols, fillCharData));
} else {
if (this.ydisp === this.ybase) {
this.ydisp--;
}
this.ybase--;
}
}
}

return wrappedLines.length - countToRemove - 1;
}

private _reflowSmaller(y: number, newCols: number): number {
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
// Check whether this line is a problem
let nextLine = this.lines.get(y) as BufferLine;
if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {
return 0;
}

// Gather wrapped lines and adjust y to be the starting line
const wrappedLines: BufferLine[] = [nextLine];
if (nextLine.isWrapped) {
while (true) {
nextLine = this.lines.get(--y) as BufferLine;
// TODO: unshift is expensive
wrappedLines.unshift(nextLine);
if (!nextLine.isWrapped || y === 0) {
break;
}
}
}

// Determine how many lines need to be inserted at the end, based on the trimmed length of
// the last wrapped line
const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();
const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength;
const linesNeeded = Math.ceil(cellsNeeded / newCols);
const linesToAdd = linesNeeded - wrappedLines.length;
const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);

// Add the new lines
const newLines: BufferLine[] = [];
for (let i = 0; i < linesToAdd; i++) {
const newLine = this.getBlankLine(this._terminal.eraseAttr(), true) as BufferLine;
newLines.push(newLine);
}
this.lines.splice(y + wrappedLines.length, 0, ...newLines);
wrappedLines.push(...newLines);

// Copy buffer data to new locations, this needs to happen backwards to do in-place
let destLineIndex = Math.floor(cellsNeeded / newCols);
let destCol = cellsNeeded % newCols;
if (destCol === 0) {
destLineIndex--;
destCol = newCols;
}
let srcLineIndex = wrappedLines.length - linesToAdd - 1;
let srcCol = lastLineLength;
while (srcLineIndex >= 0) {
const cellsToCopy = Math.min(srcCol, destCol);
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);
destCol -= cellsToCopy;
if (destCol === 0) {
destLineIndex--;
destCol = newCols;
}
srcCol -= cellsToCopy;
if (srcCol === 0) {
srcLineIndex--;
srcCol = this._terminal.cols;
}
}

// Adjust viewport as needed
let viewportAdjustments = linesToAdd - trimmedLines;
while (viewportAdjustments-- > 0) {
if (this.ybase === 0) {
if (this.y < this._terminal.rows - 1) {
this.y++;
this.lines.pop();
} else {
this.ybase++;
// TODO: Use this? if (this._terminal._userScrolling) {
this.ydisp++;
}
} else {
if (this.ybase === this.ydisp) {
this.ybase++;
this.ydisp++;
}
}
}

return wrappedLines.length - 1 - linesToAdd + trimmedLines;
}

/**
Expand Down
51 changes: 3 additions & 48 deletions src/BufferLine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,64 +141,19 @@ describe('BufferLine', function(): void {
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) - should not apply new size', function(): void {
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + shrink(false) - should not apply new size', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + enlarge(false) to smaller than before', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + enlarge(false) to bigger than before', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(25, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(25).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + resize shrink=true should enforce shrinking', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge from 0 length', function(): void {
const line = new TestBufferLine(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], true);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) to 0 and enlarge to different sizes', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
Expand Down
23 changes: 20 additions & 3 deletions src/BufferLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ export class BufferLine implements IBufferLine {
}
}

public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
if (cols === this.length || (!shrink && cols < this.length)) {
public resize(cols: number, fillCharData: CharData): void {
if (cols === this.length) {
return;
}
if (cols > this.length) {
Expand All @@ -245,7 +245,7 @@ export class BufferLine implements IBufferLine {
for (let i = this.length; i < cols; ++i) {
this.set(i, fillCharData);
}
} else if (shrink) {
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
} else {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
Expand Down Expand Up @@ -304,6 +304,23 @@ export class BufferLine implements IBufferLine {
return 0;
}

public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
const srcData = src._data;
if (applyInReverse) {
for (let cell = length - 1; cell >= 0; cell--) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
} else {
for (let cell = 0; cell < length; cell++) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
}
}

public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
if (trimRight) {
endCol = Math.min(endCol, this.getTrimmedLength());
Expand Down
1 change: 1 addition & 0 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
showCursor(): void;
eraseAttr(): number;
}

export interface IBufferAccessor {
Expand Down
3 changes: 3 additions & 0 deletions src/ui/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class TestTerminal extends Terminal {
}

export class MockTerminal implements ITerminal {
eraseAttr(): number {
throw new Error('Method not implemented.');
}
markers: IMarker[];
addMarker(cursorYOffset: number): IMarker {
throw new Error('Method not implemented.');
Expand Down