Skip to content

Commit

Permalink
Merge branch 'master' into undefinedrowscols
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge authored Sep 2, 2021
2 parents aa046b7 + b9b42ef commit 011b58d
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 311 deletions.
2 changes: 1 addition & 1 deletion addons/xterm-addon-webgl/src/WebglRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.dispose();
}
this._core.screenElement!.removeChild(this._canvas);
this._canvas.parentElement?.removeChild(this._canvas);
super.dispose();
}

Expand Down
34 changes: 34 additions & 0 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ if (document.location.pathname === '/test') {
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
document.getElementById('load-test').addEventListener('click', loadTest);
}

function createTerminal(): void {
Expand Down Expand Up @@ -481,3 +482,36 @@ function writeCustomGlyphHandler() {
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
window.scrollTo(0, 0);
}

function loadTest() {
const isWebglEnabled = !!addons.webgl.instance;
const testData = [];
let byteCount = 0;
for (let i = 0; i < 50; i++) {
const count = 1 + Math.floor(Math.random() * 79);
byteCount += count + 2;
const data = new Uint8Array(count + 2);
data[0] = 0x0A; // \n
for (let i = 1; i < count + 1; i++) {
data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61));
}
// End each line with \r so the cursor remains constant, this is what ls/tree do and improves
// performance significantly due to the cursor DOM element not needing to change
data[data.length - 1] = 0x0D; // \r
testData.push(data);
}
const start = performance.now();
for (let i = 0; i < 1024; i++) {
for (const d of testData) {
term.write(d);
}
}
// Wait for all data to be parsed before evaluating time
term.write('', () => {
const time = Math.round(performance.now() - start);
const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2);
term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${isWebglEnabled ? 'webgl' : 'canvas'} renderer)`);
// Send ^C to get a new prompt
term._core._onData.fire('\x03');
});
}
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ <h3>Test</h3>
<div style="display: inline-block; margin-right: 16px;">
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
</div>
</div>
</div>
Expand Down
142 changes: 70 additions & 72 deletions src/browser/Terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,80 +732,78 @@ describe('Terminal', () => {
});

describe('unicode - surrogates', () => {
it('2 characters per cell', async function (): Promise<void> {
this.timeout(10000); // This is needed because istanbul patches code and slows it down
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
await term.writeP(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(i));
assert.equal(tchar.getChars().length, 2);
assert.equal(tchar.getWidth(), 1);
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it('2 characters at last cell', async () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
await term.writeP(high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
term.reset();
}
});
it('2 characters per cell over line end with autowrap', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;

await term.writeP('a' + high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it('2 characters per cell over line end without autowrap', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
for (let i = 0xDC00; i <= 0xDCF0; i += 0x10) {
const range = `0x${i.toString(16).toUpperCase()}-0x${(i + 0xF).toString(16).toUpperCase()}`;
it(`${range}: 2 characters per cell`, async function (): Promise<void> {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let j = i; j <= i + 0xF; j++) {
await term.writeP(high + String.fromCharCode(j));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(j));
assert.equal(tchar.getChars().length, 2);
assert.equal(tchar.getWidth(), 1);
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it(`${range}: 2 characters at last cell`, async () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
term.buffer.x = term.cols - 1;
await term.writeP('\x1b[?7l'); // Disable wraparound mode
const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
for (let j = i; j <= i + 0xF; j++) {
await term.writeP(high + String.fromCharCode(j));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(j));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
term.reset();
}
await term.writeP('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it('splitted surrogates', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
await term.writeP(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(i));
assert.equal(tchar.getChars().length, 2);
assert.equal(tchar.getWidth(), 1);
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
});
it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise<void> {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let j = i; j <= i + 0xF; j++) {
term.buffer.x = term.cols - 1;
await term.writeP('a' + high + String.fromCharCode(j));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(j));
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise<void> {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let j = i; j <= i + 0xF; j++) {
term.buffer.x = term.cols - 1;
await term.writeP('\x1b[?7l'); // Disable wraparound mode
const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
}
await term.writeP('a' + high + String.fromCharCode(j));
// auto wraparound mode should cut off the rest of the line
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(j));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
it(`${range}: splitted surrogates`, async function (): Promise<void> {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let j = i; j <= i + 0xF; j++) {
await term.writeP(high + String.fromCharCode(j));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(j));
assert.equal(tchar.getChars().length, 2);
assert.equal(tchar.getWidth(), 1);
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
term.reset();
}
});
}
});

describe('unicode - combining characters', () => {
Expand Down
23 changes: 14 additions & 9 deletions src/browser/Viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IRenderDimensions } from 'browser/renderer/Types';

const FALLBACK_SCROLL_BAR_WIDTH = 15;

Expand All @@ -18,12 +20,15 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15;
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _currentScaledCellHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number = 0;
private _lastScrollTop: number = 0;
private _lastHadScrollBar: boolean = false;
private _activeBuffer: IBuffer;
private _renderDimensions: IRenderDimensions;

// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
Expand Down Expand Up @@ -51,6 +56,12 @@ export class Viewport extends Disposable implements IViewport {
this._lastHadScrollBar = true;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));

// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));
this._renderDimensions = this._renderService.dimensions;
this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));

// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
Expand Down Expand Up @@ -79,6 +90,7 @@ export class Viewport extends Disposable implements IViewport {
private _innerRefresh(): void {
if (this._charSizeService.height > 0) {
this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio;
this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
if (this._lastRecordedBufferHeight !== newBufferHeight) {
Expand Down Expand Up @@ -126,20 +138,13 @@ export class Viewport extends Disposable implements IViewport {
}

// If the buffer position doesn't match last scroll top
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) {
this._refresh(immediate);
return;
}

// If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) {
this._refresh(immediate);
return;
}

// If row height changed
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) {
this._refresh(immediate);
return;
}
Expand Down
Loading

0 comments on commit 011b58d

Please sign in to comment.