From 18871f0d3cf3fd41b641ec5dcfb809c3ddd82657 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 1 Oct 2018 13:30:13 -0700 Subject: [PATCH 01/64] Cleared terminal lines should have isWrapped reset Fixes #1710 --- src/InputHandler.ts | 21 ++++++++++++++++++--- src/addons/search/SearchHelper.ts | 2 +- src/addons/search/search.test.ts | 3 +++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ad5aad39..9a0aae08 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -730,6 +730,21 @@ export class InputHandler extends Disposable implements IInputHandler { ); } + /** + * Helper method to reset cells in a terminal row. + * The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false. + * @param y row index + */ + private _resetBufferLine(y: number): void { + const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); + line.replaceCells( + 0, + this._terminal.cols, + [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + ); + line.isWrapped = false; + } + /** * CSI Ps J Erase in Display (ED). * Ps = 0 -> Erase Below (default). @@ -750,7 +765,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(j); this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols); for (; j < this._terminal.rows; j++) { - this._eraseInBufferLine(j, 0, this._terminal.cols); + this._resetBufferLine(j); } this._terminal.updateRange(j); break; @@ -759,7 +774,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(j); this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1); while (j--) { - this._eraseInBufferLine(j, 0, this._terminal.cols); + this._resetBufferLine(j); } this._terminal.updateRange(0); break; @@ -767,7 +782,7 @@ export class InputHandler extends Disposable implements IInputHandler { j = this._terminal.rows; this._terminal.updateRange(j - 1); while (j--) { - this._eraseInBufferLine(j, 0, this._terminal.cols); + this._resetBufferLine(j); } this._terminal.updateRange(0); break; diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index d07c6b2c..da09f83f 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -191,7 +191,7 @@ export class SearchHelper implements ISearchHelper { do { const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1); lineWrapsToNext = nextLine ? nextLine.isWrapped : false; - lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight); + lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols); lineIndex++; } while (lineWrapsToNext); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index ccdebaf3..b22c22a5 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -82,6 +82,9 @@ describe('search addon', () => { expect(hello3).eql(undefined); expect(llo).eql(undefined); expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'}); + term.core.resize(9, 5); + const hello0Resize = term.searchHelper.findInLine('Hello', 0); + expect(hello0Resize).eql({col: 8, row: 0, term: 'Hello'}); }); it('should respect search regex', () => { search.apply(MockTerminal); From 0065682bd7ebda786628f16329ae88c0a5c52abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 9 Oct 2018 21:17:33 +0200 Subject: [PATCH 02/64] resize should not alter length if shrink=false --- src/BufferLine.test.ts | 92 ++++++++++++++++++++++++++++++------------ src/BufferLine.ts | 22 +++++----- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 2db69176..b824cc38 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -133,31 +133,71 @@ describe('BufferLine', function(): void { const line3 = line.clone(); chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray()); }); - it('resize enlarge', function(): void { - const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('resize shrink(true)', function(): void { - const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true); - chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('resize shrink(false)', function(): void { - 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(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('resize shrink(false) + shrink(false)', 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(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('resize shrink(false) + enlarge', 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(15).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + describe('resize', function(): void { + it('enlarge(false)', function(): void { + const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); + line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); + chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + }); + 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); + 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); + chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + }); + it('shrink(false) - should not 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)], 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); + 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)])); + }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 53f5d4d4..f1ea9cf0 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -77,15 +77,15 @@ export class BufferLine implements IBufferLine { /** resize line to cols filling new cells with fill */ public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { + while (this._data.length < cols) { + this._data.push(fillCharData); + } if (shrink) { while (this._data.length > cols) { this._data.pop(); } } - while (this._data.length < cols) { - this._data.push(fillCharData); - } - this.length = cols; + this.length = this._data.length; } public fill(fillCharData: CharData): void { @@ -141,11 +141,13 @@ export class BufferLineTypedArray implements IBufferLine { if (!fillCharData) { fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; } - this._data = new Uint32Array(cols * CELL_SIZE); - for (let i = 0; i < cols; ++i) { - this.set(i, fillCharData); + if (cols) { + this._data = new Uint32Array(cols * CELL_SIZE); + for (let i = 0; i < cols; ++i) { + this.set(i, fillCharData); + } } - this.length = cols || 0; + this.length = cols; } public get(index: number): CharData { @@ -212,12 +214,12 @@ export class BufferLineTypedArray implements IBufferLine { } public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { - if (cols === this.length) { + if (cols === this.length || (!shrink && cols < this.length)) { return; } if (cols > this.length) { const data = new Uint32Array(cols * CELL_SIZE); - if (this._data) { + if (this.length) { if (cols * CELL_SIZE < this._data.length) { data.set(this._data.subarray(0, cols * CELL_SIZE)); } else { From 33a358033ca26fd9d7d54f01127446357fac8b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 9 Oct 2018 23:48:58 +0200 Subject: [PATCH 03/64] new approach --- src/BufferLine.ts | 11 +++++------ src/Terminal.ts | 17 ++++++++++++----- src/common/CircularList.ts | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 53f5d4d4..4134673e 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -94,13 +94,11 @@ export class BufferLine implements IBufferLine { } } - public copyFrom(line: IBufferLine): void { - this._data = []; - for (let i = 0; i < line.length; ++i) { - this._push(line.get(i)); - } + public copyFrom(line: BufferLine): IBufferLine { + this._data = line._data.slice(0); this.length = line.length; this.isWrapped = line.isWrapped; + return this; } public clone(): IBufferLine { @@ -249,7 +247,7 @@ export class BufferLineTypedArray implements IBufferLine { } /** alter to a full copy of line */ - public copyFrom(line: BufferLineTypedArray): void { + public copyFrom(line: BufferLineTypedArray): IBufferLine { if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { @@ -262,6 +260,7 @@ export class BufferLineTypedArray implements IBufferLine { this._combined[el] = line._combined[el]; } this.isWrapped = line.isWrapped; + return this; } /** create a new clone */ diff --git a/src/Terminal.ts b/src/Terminal.ts index c995af62..919b039e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1174,7 +1174,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(isWrapped?: boolean): void { - const newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); + // TODO: make blank a member + let blank: IBufferLine = (this as any)._blank; + if (!blank || blank.length !== this.cols) { + blank = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); + (this as any)._blank = blank; + } + blank.isWrapped = !!(isWrapped); + const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1184,9 +1191,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { - this.buffer.lines.push(newLine); + (this.buffer.lines as any).pushRecycling((line: IBufferLine | undefined) => (line) ? line.copyFrom(blank) : blank.clone()); } else { - this.buffer.lines.splice(bottomRow + 1, 0, newLine); + this.buffer.lines.splice(bottomRow + 1, 0, blank.clone()); } // Only adjust ybase and ydisp when the buffer is not trimmed @@ -1208,7 +1215,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, newLine); + this.buffer.lines.set(bottomRow, blank.clone()); } // Move the viewport to the bottom of the buffer unless the user is diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 542dbf12..9609960d 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -100,6 +100,24 @@ export class CircularList extends EventEmitter implements ICircularList { } } + /** + * Recycling push variant with a callback. + * The callback gets the value at the current position to be overwritten. + * Return the new value from the callback. + */ + public pushRecycling(callback: (el: T | undefined) => T): void { + this._array[this._getCyclicIndex(this._length)] = callback(this._array[this._getCyclicIndex(this._length)]); + if (this._length === this._maxLength) { + this._startIndex++; + if (this._startIndex === this._maxLength) { + this._startIndex = 0; + } + this.emit('trim', 1); + } else { + this._length++; + } + } + /** * Removes and returns the last value on the list. * @return The popped value. From 6e350219aaac512c23493252dc556e27cee87518 Mon Sep 17 00:00:00 2001 From: leomoty Date: Mon, 8 Oct 2018 23:17:45 -0300 Subject: [PATCH 04/64] Support for lineHeight in DOM Renderer --- src/renderer/dom/DomRenderer.ts | 27 ++++++++++++++------------- typings/xterm.d.ts | 1 - 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 8f51c4ef..bd0a6b02 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -94,22 +94,23 @@ export class DomRenderer extends EventEmitter implements IRenderer { } private _updateDimensions(): void { - this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; - this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio; - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth; - this.dimensions.scaledCellHeight = this.dimensions.scaledCharHeight; + this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio); + this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); this.dimensions.scaledCharLeft = 0; this.dimensions.scaledCharTop = 0; this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; - this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols; - this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows; - this.dimensions.actualCellWidth = this._terminal.charMeasure.width; - this.dimensions.actualCellHeight = this._terminal.charMeasure.height; + this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); + this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; this._rowElements.forEach(element => { element.style.width = `${this.dimensions.canvasWidth}px`; - element.style.height = `${this._terminal.charMeasure.height}px`; + element.style.height = `${this.dimensions.actualCellHeight}px`; + element.style.lineHeight = `${this.dimensions.actualCellHeight}px`; }); if (!this._dimensionsStyleElement) { @@ -290,10 +291,10 @@ export class DomRenderer extends EventEmitter implements IRenderer { */ private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement { const element = document.createElement('div'); - element.style.height = `${rowCount * this._terminal.charMeasure.height}px`; - element.style.top = `${row * this._terminal.charMeasure.height}px`; - element.style.left = `${colStart * this._terminal.charMeasure.width}px`; - element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`; + element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`; + element.style.top = `${row * this.dimensions.actualCellHeight}px`; + element.style.left = `${colStart * this.dimensions.actualCellWidth}px`; + element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`; return element; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b5a8a109..f809c2ed 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -161,7 +161,6 @@ declare module 'xterm' { * when canvas is too slow for the environment. The following features do * not work when the DOM renderer is used: * - * - Line height * - Letter spacing * - Cursor blink */ From a63779ae7b12d7b8eb4f408d5a182fc784851f31 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Wed, 10 Oct 2018 10:26:32 +0300 Subject: [PATCH 05/64] Fix docker image build. Signed-off-by: Oleksandr Andriienko --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1e3a262c..1c72e679 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ RUN npm install COPY . /usr/src/app # Run the tests and build, to make sure everything is working nicely -RUN npm run build && npm run webpack && npm run test +RUN npm run build && npm run test From 45e64a9167aafc8a6e49743ca3f4450a07d8726b Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Wed, 10 Oct 2018 11:39:34 +0300 Subject: [PATCH 06/64] Remove from package.json unused script command Signed-off-by: Oleksandr Andriienko --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index b10b6b54..ad763f0b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "webpack": "gulp webpack", "watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", "watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", "layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\"" From c739d2fe870ac911674f9f6955daf21e9068545b Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 10 Oct 2018 14:05:23 +0200 Subject: [PATCH 07/64] Remove underscore from non word characters list This is the upstream fix for Microsoft/vscode#59379 --- src/addons/search/SearchHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index d07c6b2c..f23a8c58 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -4,7 +4,7 @@ */ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; -const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?'; +const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; /** * A class that knows how to search the terminal and how to display the results. From 751e2efa755d7d5e7b45474725676bf0b2d0100c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 10 Oct 2018 15:00:38 +0200 Subject: [PATCH 08/64] better integration and experimentalPushRecycling option --- src/Terminal.ts | 34 ++++++++++++++++++++++++---------- src/Types.ts | 2 +- src/common/CircularList.ts | 2 +- src/common/Types.ts | 1 + typings/xterm.d.ts | 2 ++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 919b039e..ff820982 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -106,7 +106,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { theme: null, rightClickSelectsWord: Browser.isMac, rendererType: 'canvas', - experimentalBufferLineImpl: 'JsArray' + experimentalBufferLineImpl: 'JsArray', + experimentalPushRecycling: false }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -208,6 +209,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; + // bufferline to clone/copy from for new blank lines + private _blankLine: IBufferLine = null; + public cols: number; public rows: number; @@ -496,6 +500,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'experimentalBufferLineImpl': this.buffers.normal.setBufferLineFactory(value); this.buffers.alt.setBufferLineFactory(value); + this._blankLine = null; break; } // Inform renderer of changes @@ -1174,13 +1179,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(isWrapped?: boolean): void { - // TODO: make blank a member - let blank: IBufferLine = (this as any)._blank; - if (!blank || blank.length !== this.cols) { - blank = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); - (this as any)._blank = blank; + let newLine: IBufferLine; + const useRecycling = this.options.experimentalPushRecycling; + if (useRecycling) { + newLine = this._blankLine; + if (!newLine || newLine.length !== this.cols) { + newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); + this._blankLine = newLine; + } + newLine.isWrapped = !!(isWrapped); + } else { + newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); } - blank.isWrapped = !!(isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1191,9 +1201,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { - (this.buffer.lines as any).pushRecycling((line: IBufferLine | undefined) => (line) ? line.copyFrom(blank) : blank.clone()); + if (useRecycling) { + this.buffer.lines.pushRecycling((item) => (item) ? item.copyFrom(newLine) : newLine.clone()); + } else { + this.buffer.lines.push(newLine); + } } else { - this.buffer.lines.splice(bottomRow + 1, 0, blank.clone()); + this.buffer.lines.splice(bottomRow + 1, 0, (useRecycling) ? newLine.clone() : newLine); } // Only adjust ybase and ydisp when the buffer is not trimmed @@ -1215,7 +1229,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, blank.clone()); + this.buffer.lines.set(bottomRow, (useRecycling) ? newLine.clone() : newLine); } // Move the viewport to the bottom of the buffer unless the user is diff --git a/src/Types.ts b/src/Types.ts index e8578426..cafe60db 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -522,7 +522,7 @@ export interface IBufferLine { replaceCells(start: number, end: number, fill: CharData): void; resize(cols: number, fill: CharData, shrink?: boolean): void; fill(fillCharData: CharData): void; - copyFrom(line: IBufferLine): void; + copyFrom(line: IBufferLine): IBufferLine; clone(): IBufferLine; } diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 9609960d..cc0809ed 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -105,7 +105,7 @@ export class CircularList extends EventEmitter implements ICircularList { * The callback gets the value at the current position to be overwritten. * Return the new value from the callback. */ - public pushRecycling(callback: (el: T | undefined) => T): void { + public pushRecycling(callback: (item: T | undefined) => T): void { this._array[this._getCyclicIndex(this._length)] = callback(this._array[this._getCyclicIndex(this._length)]); if (this._length === this._maxLength) { this._startIndex++; diff --git a/src/common/Types.ts b/src/common/Types.ts index aabe721e..ec6fcfd5 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -28,6 +28,7 @@ export interface ICircularList extends IEventEmitter { get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; + pushRecycling(callback: (item: T | undefined) => T): void; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b5a8a109..7fb33a9b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -112,6 +112,8 @@ declare module 'xterm' { */ experimentalBufferLineImpl?: 'JsArray' | 'TypedArray'; + experimentalPushRecycling?: boolean; + /** * The font size used to render text. */ From f2ecd06e64dcbfdc3cbfdc24bac59f52d1255569 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 10 Oct 2018 15:31:49 +0200 Subject: [PATCH 09/64] Additional isWrapped clearing --- src/InputHandler.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 9a0aae08..97001d0a 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -722,12 +722,16 @@ export class InputHandler extends Disposable implements IInputHandler { * @param start first cell index to be erased * @param end end - 1 is last erased cell */ - private _eraseInBufferLine(y: number, start: number, end: number): void { - this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y).replaceCells( + private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void { + const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); + line.replaceCells( start, end, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] ); + if (clearWrap) { + line.isWrapped = false; + } } /** @@ -736,13 +740,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @param y row index */ private _resetBufferLine(y: number): void { - const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); - line.replaceCells( - 0, - this._terminal.cols, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] - ); - line.isWrapped = false; + this._eraseInBufferLine(y, 0, this._terminal.cols, true); } /** @@ -763,7 +761,11 @@ export class InputHandler extends Disposable implements IInputHandler { case 0: j = this._terminal.buffer.y; this._terminal.updateRange(j); - this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols); + if (this._terminal.buffer.x !== 0) { + this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols); + } else { + this._resetBufferLine(j++); + } for (; j < this._terminal.rows; j++) { this._resetBufferLine(j); } From eb123ca787a21cd634d08e6e05ff0bd7eedc39e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Oct 2018 00:32:14 +0200 Subject: [PATCH 10/64] add tiny buffer to server part in demo --- demo/server.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/demo/server.js b/demo/server.js index 0aa9deef..ebe2f644 100644 --- a/demo/server.js +++ b/demo/server.js @@ -58,9 +58,25 @@ app.ws('/terminals/:pid', function (ws, req) { console.log('Connected to terminal ' + term.pid); ws.send(logs[term.pid]); + function buffer(socket, timeout) { + let s = ''; + let sender = null; + return (data) => { + s += data; + if (!sender) { + sender = setTimeout(() => { + socket.send(s); + s = ''; + sender = null; + }, timeout); + } + }; + } + const send = buffer(ws, 5); + term.on('data', function(data) { try { - ws.send(data); + send(data); } catch (ex) { // The WebSocket is not open, ignore } From 8843b0cbc6135d690d6827b044649eb70e92b94d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 10 Oct 2018 18:20:12 -0700 Subject: [PATCH 11/64] Update Azure data strudio info --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b9b877c..17833598 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. - [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. -- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux +- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. - [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. - [**Hyper**](https://hyper.is): A terminal built on web technologies From 64d6354983600b7323b49d875405ff38f2379f18 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 11 Oct 2018 11:18:56 +0200 Subject: [PATCH 12/64] Added testing and better erase left and above handling --- src/InputHandler.test.ts | 30 ++++++++++++++++++++++++++++++ src/InputHandler.ts | 13 +++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 722cc287..0e610fc0 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -444,6 +444,36 @@ describe('InputHandler', () => { termNew.buffer.x = 40; inputHandlerNew.eraseInDisplay([2]); expect(termContent(termNew)).eql(termContent(termOld)); + + // reset and add a wrapped line + termNew.buffer.y = 0; + termNew.buffer.x = 0; + inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0 + inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); + + // params[1] left and above with wrap + // confirm precondition that line 2 is wrapped + expect(termNew.buffer.lines.get(2).isWrapped).true; + termNew.buffer.y = 2; + termNew.buffer.x = 40; + inputHandlerNew.eraseInDisplay([1]); + expect(termNew.buffer.lines.get(2).isWrapped).false; + + // reset and add a wrapped line + termNew.buffer.y = 0; + termNew.buffer.x = 0; + inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0 + inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); + + // params[1] left and above with wrap + // confirm precondition that line 2 is wrapped + expect(termNew.buffer.lines.get(2).isWrapped).true; + termNew.buffer.y = 1; + termNew.buffer.x = 90; // Cursor is beyond last column + inputHandlerNew.eraseInDisplay([1]); + expect(termNew.buffer.lines.get(2).isWrapped).false; }); }); it('convertEol setting', function(): void { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 97001d0a..2fcfac27 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -761,11 +761,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 0: j = this._terminal.buffer.y; this._terminal.updateRange(j); - if (this._terminal.buffer.x !== 0) { - this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols); - } else { - this._resetBufferLine(j++); - } + this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols, this._terminal.buffer.x === 0); for (; j < this._terminal.rows; j++) { this._resetBufferLine(j); } @@ -774,7 +770,12 @@ export class InputHandler extends Disposable implements IInputHandler { case 1: j = this._terminal.buffer.y; this._terminal.updateRange(j); - this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1); + // Deleted front part of line and everything before. This line will no longer be wrapped. + this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1, true); + if (this._terminal.buffer.x + 1 >= this._terminal.cols) { + // Deleted entire previous line. This next line can no longer be wrapped. + this._terminal.buffer.lines.get(j + 1).isWrapped = false; + } while (j--) { this._resetBufferLine(j); } From 375b5c5b4f1334ea06ad64d2a22af2c00d031e33 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 11 Oct 2018 09:02:36 -0700 Subject: [PATCH 13/64] Fix indent --- src/InputHandler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 0e610fc0..04b7a173 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -461,7 +461,7 @@ describe('InputHandler', () => { expect(termNew.buffer.lines.get(2).isWrapped).false; // reset and add a wrapped line - termNew.buffer.y = 0; + termNew.buffer.y = 0; termNew.buffer.x = 0; inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0 inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2 From 9e1551e2f14f76d098b2b8d293dd6c3ef57d7255 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 11 Oct 2018 09:37:19 -0700 Subject: [PATCH 14/64] Fix NPE in DOM renderer underline Fixes #1747 --- src/renderer/dom/DomRenderer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 8f51c4ef..adce57bc 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -352,7 +352,11 @@ export class DomRenderer extends EventEmitter implements IRenderer { private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void { while (x !== x2 || y !== y2) { - const span = this._rowElements[y].children[x]; + const row = this._rowElements[y]; + if (!row) { + return; + } + const span = row.children[x]; span.style.textDecoration = enabled ? 'underline' : 'none'; x = (x + 1) % cols; if (x === 0) { From a0250c5cea4c8c310c9f5b3dca7a630786465c6a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 12 Oct 2018 08:33:59 -0700 Subject: [PATCH 15/64] Improve types for listener args in .d.ts Without this consumers need an undefined check with strictNullChecks turned on. --- typings/xterm.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b5a8a109..8676a458 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -388,37 +388,37 @@ declare module 'xterm' { * @param type The type of the event. * @param listener The listener. */ - on(type: 'key', listener: (key?: string, event?: KeyboardEvent) => void): void; + on(type: 'key', listener: (key: string, event: KeyboardEvent) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. */ - on(type: 'keypress' | 'keydown', listener: (event?: KeyboardEvent) => void): void; + on(type: 'keypress' | 'keydown', listener: (event: KeyboardEvent) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. */ - on(type: 'refresh', listener: (data?: {start: number, end: number}) => void): void; + on(type: 'refresh', listener: (data: {start: number, end: number}) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. */ - on(type: 'resize', listener: (data?: {cols: number, rows: number}) => void): void; + on(type: 'resize', listener: (data: {cols: number, rows: number}) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. */ - on(type: 'scroll', listener: (ydisp?: number) => void): void; + on(type: 'scroll', listener: (ydisp: number) => void): void; /** * Registers an event listener. * @param type The type of the event. * @param listener The listener. */ - on(type: 'title', listener: (title?: string) => void): void; + on(type: 'title', listener: (title: string) => void): void; /** * Registers an event listener. * @param type The type of the event. From 19f6c0a940e1d3e43e66ced78089ec1d976a72d2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 12 Oct 2018 09:01:56 -0700 Subject: [PATCH 16/64] Fix tests --- src/utils/TestUtils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index f575855a..353e615f 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -121,7 +121,7 @@ export class MockTerminal implements ITerminal { handler(data: string): void { throw new Error('Method not implemented.'); } - on(event: string, callback: () => void): void { + on(event: string, callback: (...args: any[]) => void): void { throw new Error('Method not implemented.'); } off(type: string, listener: XtermListener): void { From f61ba9558b6820c52030aee122a8042518bc4a13 Mon Sep 17 00:00:00 2001 From: pkoushik Date: Mon, 15 Oct 2018 23:44:30 +0530 Subject: [PATCH 17/64] Fixes-1750 --- src/addons/search/SearchHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index f23a8c58..7dea831c 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -205,6 +205,7 @@ export class SearchHelper implements ISearchHelper { */ private _selectResult(result: ISearchResult): boolean { if (!result) { + this._terminal.clearSelection(); return false; } this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length); From a53aaa5f25eec4e6e050b357f80d9ae3148aec61 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 15 Oct 2018 11:35:21 -0700 Subject: [PATCH 18/64] Move dynamic atlas canCache check higher Fixes #1757 --- src/renderer/atlas/DynamicCharAtlas.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index e67fe4f0..63336900 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -129,12 +129,17 @@ export default class DynamicCharAtlas extends BaseCharAtlas { return true; } + // Exit early for uncachable glyphs + if (!this._canCache(glyph)) { + return false; + } + const glyphKey = getGlyphCacheKey(glyph); const cacheValue = this._cacheMap.get(glyphKey); if (cacheValue !== null && cacheValue !== undefined) { this._drawFromCache(ctx, cacheValue, x, y); return true; - } else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) { + } else if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) { let index; if (this._cacheMap.size < this._cacheMap.capacity) { index = this._cacheMap.size; From 161dfa8d2d7c516b49649f623f93b913c8316189 Mon Sep 17 00:00:00 2001 From: Stathis Kavros Date: Tue, 16 Oct 2018 14:23:58 +0300 Subject: [PATCH 19/64] Fix typos --- src/addons/search/SearchHelper.ts | 6 +++--- src/addons/search/search.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7dea831c..9f59a9fd 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -19,7 +19,7 @@ export class SearchHelper implements ISearchHelper { /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. - * @param term Tne search term. + * @param term The search term. * @param searchOptions Search options. * @return Whether a result was found. */ @@ -61,7 +61,7 @@ export class SearchHelper implements ISearchHelper { /** * Find the previous instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. - * @param term Tne search term. + * @param term The search term. * @param searchOptions Search options. * @return Whether a result was found. */ @@ -116,7 +116,7 @@ export class SearchHelper implements ISearchHelper { * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * text starts on is searched. - * @param term Tne search term. + * @param term The search term. * @param y The line to search. * @param searchOptions Search options. * @return The search result if it was found. diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index 9be0b33b..c274f28a 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -10,7 +10,7 @@ import { ISearchAddonTerminal, ISearchOptions } from './Interfaces'; /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. - * @param term Tne search term. + * @param term The search term. * @param searchOptions Search options * @return Whether a result was found. */ @@ -25,7 +25,7 @@ export function findNext(terminal: Terminal, term: string, searchOptions: ISearc /** * Find the previous instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. - * @param term Tne search term. + * @param term The search term. * @param searchOptions Search options * @return Whether a result was found. */ From 3f3263cbb04b1e4669b1e7440979f6069ffb092d Mon Sep 17 00:00:00 2001 From: Vladimir Zeifman Date: Sat, 13 Oct 2018 19:10:56 +0300 Subject: [PATCH 20/64] Fix tslint errors for all addons and un-exclude tests for search addon --- src/addons/attach/attach.test.ts | 2 +- src/addons/fit/fit.test.ts | 2 +- src/addons/fullscreen/fullscreen.test.ts | 2 +- src/addons/fullscreen/fullscreen.ts | 11 ++++++----- src/addons/search/search.test.ts | 1 + src/addons/search/tsconfig.json | 3 --- src/addons/terminado/terminado.test.ts | 2 +- src/addons/webLinks/webLinks.test.ts | 2 +- src/addons/winptyCompat/winptyCompat.test.ts | 2 +- src/addons/zmodem/zmodem.test.ts | 2 +- src/addons/zmodem/zmodem.ts | 4 ++-- 11 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts index 018cfb31..e280b656 100644 --- a/src/addons/attach/attach.test.ts +++ b/src/addons/attach/attach.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as attach from './attach'; diff --git a/src/addons/fit/fit.test.ts b/src/addons/fit/fit.test.ts index 9a6d89fd..781b5010 100644 --- a/src/addons/fit/fit.test.ts +++ b/src/addons/fit/fit.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as fit from './fit'; diff --git a/src/addons/fullscreen/fullscreen.test.ts b/src/addons/fullscreen/fullscreen.test.ts index bb98bd30..6d41bdfd 100644 --- a/src/addons/fullscreen/fullscreen.test.ts +++ b/src/addons/fullscreen/fullscreen.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as fullscreen from './fullscreen'; diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 297a7f5b..ef9b8ffd 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -11,17 +11,18 @@ import { Terminal } from 'xterm'; * @param fullscreen Toggle fullscreen on (true) or off (false) */ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { - let fn: string; + let fn: Function; if (typeof fullscreen === 'undefined') { - fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add'; + fn = (term.element.classList.contains('fullscreen')) ? + term.element.classList.remove : term.element.classList.add; } else if (!fullscreen) { - fn = 'remove'; + fn = term.element.classList.remove; } else { - fn = 'add'; + fn = term.element.classList.add; } - term.element.classList[fn]('fullscreen'); + fn('fullscreen'); } export function apply(terminalConstructor: typeof Terminal): void { diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 1fe18b6a..7451d428 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -2,6 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ +declare var require: any; import { assert, expect } from 'chai'; import * as search from './search'; diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 9998dc1b..c34a0bc5 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -18,8 +18,5 @@ }, "include": [ "**/*.ts" - ], - "exclude": [ - "**/*.test.ts" ] } diff --git a/src/addons/terminado/terminado.test.ts b/src/addons/terminado/terminado.test.ts index 2e4a53c5..e46eafdf 100644 --- a/src/addons/terminado/terminado.test.ts +++ b/src/addons/terminado/terminado.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as terminado from './terminado'; diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index c84ee1a5..014c25e4 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as webLinks from './webLinks'; diff --git a/src/addons/winptyCompat/winptyCompat.test.ts b/src/addons/winptyCompat/winptyCompat.test.ts index 0c9269ed..c3a7e479 100644 --- a/src/addons/winptyCompat/winptyCompat.test.ts +++ b/src/addons/winptyCompat/winptyCompat.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as winptyCompat from './winptyCompat'; diff --git a/src/addons/zmodem/zmodem.test.ts b/src/addons/zmodem/zmodem.test.ts index 682e62c8..d0c7c5fb 100644 --- a/src/addons/zmodem/zmodem.test.ts +++ b/src/addons/zmodem/zmodem.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as zmodem from './zmodem'; diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts index 23204f0f..70fc6e98 100644 --- a/src/addons/zmodem/zmodem.ts +++ b/src/addons/zmodem/zmodem.ts @@ -34,7 +34,7 @@ import { Terminal } from 'xterm'; * via `detach()` and a re-`attach()`.) */ -let zmodem; +let zmodem: any; export interface IZmodemOptions { noTerminalWriteOutsideSession?: boolean; @@ -44,7 +44,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { const term = this; const senderFunc = (octets: ArrayLike) => ws.send(new Uint8Array(octets)); - let zsentry; + let zsentry: any; function shouldWrite(): boolean { return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; From 74664a91611d4ae718012097f11e0f58f65989e9 Mon Sep 17 00:00:00 2001 From: pkoushik Date: Wed, 17 Oct 2018 10:48:46 +0530 Subject: [PATCH 21/64] Fix-1749 --- src/addons/search/SearchHelper.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7dea831c..6829ef4c 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -33,7 +33,9 @@ export class SearchHelper implements ISearchHelper { let startRow = this._terminal._core.buffer.ydisp; if (this._terminal._core.selectionManager.selectionEnd) { // Start from the selection end if there is a selection - startRow = this._terminal._core.selectionManager.selectionEnd[1]; + if (this._terminal.getSelection().length !== 0) { + startRow = this._terminal._core.selectionManager.selectionEnd[1]; + } } // Search from ydisp + 1 to end @@ -75,7 +77,9 @@ export class SearchHelper implements ISearchHelper { let startRow = this._terminal._core.buffer.ydisp; if (this._terminal._core.selectionManager.selectionStart) { // Start from the selection end if there is a selection - startRow = this._terminal._core.selectionManager.selectionStart[1]; + if (this._terminal.getSelection().length !== 0) { + startRow = this._terminal._core.selectionManager.selectionStart[1]; + } } // Search from ydisp + 1 to end @@ -108,7 +112,7 @@ export class SearchHelper implements ISearchHelper { */ private _isWholeWord(searchIndex: number, line: string, term: string): boolean { return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) && - (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); + (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); } /** From 8bd49cba35617699f6c69bf7b648b66b6cb17057 Mon Sep 17 00:00:00 2001 From: Jeff Smith Date: Fri, 21 Sep 2018 23:20:38 -0500 Subject: [PATCH 22/64] Fill inserted lines with background color When scrolling occurs due to a LF or scroll code, fill the new line with the currently-set background color. --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c995af62..55e212e0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1174,7 +1174,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(isWrapped?: boolean): void { - const newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); + const newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; From 35db216f649006ca5d35dd1d51a1f5f0d5841ea9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 20 Oct 2018 13:55:13 -0700 Subject: [PATCH 23/64] Fix indent --- src/addons/search/SearchHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index cd867824..938887d8 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -112,7 +112,7 @@ export class SearchHelper implements ISearchHelper { */ private _isWholeWord(searchIndex: number, line: string, term: string): boolean { return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) && - (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); + (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); } /** From 82228ad8913d85faeb442c76a9da29ffd0895917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 22 Oct 2018 18:15:47 +0200 Subject: [PATCH 24/64] trimAndRecycle with external check --- src/Terminal.ts | 7 ++++++- src/common/CircularList.ts | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index ff820982..516aa2a0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1202,7 +1202,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { if (useRecycling) { - this.buffer.lines.pushRecycling((item) => (item) ? item.copyFrom(newLine) : newLine.clone()); + // this.buffer.lines.pushRecycling((item) => (item) ? item.copyFrom(newLine) : newLine.clone()); + if (willBufferBeTrimmed) { + (this.buffer.lines as any).trimAndRecycle().copyFrom(newLine); + } else { + this.buffer.lines.push(newLine.clone()); + } } else { this.buffer.lines.push(newLine); } diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index cc0809ed..682b8775 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -118,6 +118,15 @@ export class CircularList extends EventEmitter implements ICircularList { } } + public trimAndRecycle(): T | undefined { + this._startIndex++; + if (this._startIndex === this._maxLength) { + this._startIndex = 0; + } + this.emit('trim', 1); + return this._array[this._getCyclicIndex(this._length - 1)]; + } + /** * Removes and returns the last value on the list. * @return The popped value. @@ -154,10 +163,10 @@ export class CircularList extends EventEmitter implements ICircularList { } // Adjust length as needed - if (this._length + items.length > this.maxLength) { - const countToTrim = (this._length + items.length) - this.maxLength; + if (this._length + items.length > this._maxLength) { + const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; - this._length = this.maxLength; + this._length = this._maxLength; this.emit('trim', countToTrim); } else { this._length += items.length; @@ -196,7 +205,7 @@ export class CircularList extends EventEmitter implements ICircularList { const expandListBy = (start + count + offset) - this._length; if (expandListBy > 0) { this._length += expandListBy; - while (this._length > this.maxLength) { + while (this._length > this._maxLength) { this._length--; this._startIndex++; this.emit('trim', 1); @@ -216,6 +225,6 @@ export class CircularList extends EventEmitter implements ICircularList { * @returns The cyclic index. */ private _getCyclicIndex(index: number): number { - return (this._startIndex + index) % this.maxLength; + return (this._startIndex + index) % this._maxLength; } } From 1585aba5b5c174a7f9dc88395df87ac2180c4b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 22 Oct 2018 21:12:51 +0200 Subject: [PATCH 25/64] slightly faster trimAndRecycle --- src/common/CircularList.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 682b8775..48394c85 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -119,10 +119,7 @@ export class CircularList extends EventEmitter implements ICircularList { } public trimAndRecycle(): T | undefined { - this._startIndex++; - if (this._startIndex === this._maxLength) { - this._startIndex = 0; - } + this._startIndex = ++this._startIndex % this._maxLength; this.emit('trim', 1); return this._array[this._getCyclicIndex(this._length - 1)]; } From 1208fc9e106513a4979d054029c49765b8f98ff1 Mon Sep 17 00:00:00 2001 From: jrkong Date: Mon, 22 Oct 2018 16:32:04 -0400 Subject: [PATCH 26/64] updated webLinks regex to fix vscode issue#60401 --- src/addons/webLinks/webLinks.test.ts | 4 ++-- src/addons/webLinks/webLinks.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index c84ee1a5..1ae62b80 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -32,11 +32,11 @@ describe('webLinks addon', () => { const term = new MockTerminal(); webLinks.webLinksInit(term); - const row = ' http://foo.com/a~b#c~d?e~f '; + const row = ' http://foo.com/a~b#c~d?e~f: '; const match = row.match(term.regex); const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + assert.equal(uri, 'http://foo.com/a~b#c~d?e~f:'); }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index a007bbd6..382d5dba 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; +const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; From 4d556fcaf0b6158d5327ef6df61b908bb01893db Mon Sep 17 00:00:00 2001 From: Jeff Smith Date: Tue, 23 Oct 2018 11:57:20 -0500 Subject: [PATCH 27/64] Check for focus on document as well as on textarea When clicking outside of the browser document area, the cursor does not appear to get 'blurred'. This can be confusing as it implies that the cursor is still active. This is caused by Terminal's isFocused getter only checking if the textarea is the active element of the document. Expand isFocused to also check that the document itself has focus. --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c995af62..a7b87027 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -342,7 +342,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } public get isFocused(): boolean { - return document.activeElement === this.textarea; + return document.activeElement === this.textarea && document.hasFocus(); } /** From c2994b0ae1b72fffe95312ef74c5d32897ac82a7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 23 Oct 2018 15:45:52 -0700 Subject: [PATCH 28/64] Remove request term info handler We're not supporting it anyway, no point having it. --- src/InputHandler.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ddb20527..a282031b 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -24,26 +24,6 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * DCS subparser implementations */ - /** - * DCS + q Pt ST (xterm) - * Request Terminfo String - * not supported - */ -class RequestTerminfo implements IDcsHandler { - private _data: string; - constructor(private _terminal: any) { } - hook(collect: string, params: number[], flag: number): void { - this._data = ''; - } - put(data: string, start: number, end: number): void { - this._data += data.substring(start, end); - } - unhook(): void { - // invalid: DCS 0 + r Pt ST - this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`); - } -} - /** * DCS $ q Pt ST * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) @@ -86,7 +66,7 @@ class DECRQSS implements IDcsHandler { default: // invalid: DCS 0 $ r Pt ST (xterm) this._terminal.error('Unknown DCS $q %s', this._data); - this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`); + this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`); } } } @@ -287,7 +267,6 @@ export class InputHandler extends Disposable implements IInputHandler { * DCS handler */ this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); - this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); } public dispose(): void { From 2efd28ad4b32593bf3403f84399c9cf1698c8c15 Mon Sep 17 00:00:00 2001 From: jrkong Date: Thu, 25 Oct 2018 00:14:20 -0400 Subject: [PATCH 29/64] updated webLinks regex to ignore colon at the end of a line and added the corresponding tests --- src/addons/webLinks/webLinks.test.ts | 28 ++++++++++++++++++++++++++-- src/addons/webLinks/webLinks.ts | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index 1ae62b80..ce9b8b44 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -32,11 +32,35 @@ describe('webLinks addon', () => { const term = new MockTerminal(); webLinks.webLinksInit(term); - const row = ' http://foo.com/a~b#c~d?e~f: '; + const row = ' http://foo.com/a~b#c~d?e~f '; const match = row.match(term.regex); const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/a~b#c~d?e~f:'); + assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + }); + + it('should allow : character in URI path', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/colon:test '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/colon:test'); + }); + + it('should not allow : character at the end of a URI path', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/colon:test: '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/colon:test'); }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 382d5dba..75d79104 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*'; +const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; From 213dadf1bd12a5ca796214a796f4c6443b5b75b5 Mon Sep 17 00:00:00 2001 From: Shengdun Hua Date: Thu, 25 Oct 2018 23:01:03 +0800 Subject: [PATCH 30/64] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 17833598..551a94f4 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. - [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. +- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 93d7a3021d22f1940f1e3ecd58ff663854bfcec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Oct 2018 16:11:47 +0200 Subject: [PATCH 31/64] remove pushRecycling, test case for trimAndRecycle, docs --- src/Terminal.ts | 17 +++++++---------- src/common/CircularList.test.ts | 16 ++++++++++++++++ src/common/CircularList.ts | 23 +++++++---------------- src/common/Types.ts | 2 +- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 9e79f3a8..5b79750c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,7 +25,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; @@ -1179,23 +1179,19 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(isWrapped?: boolean): void { -<<<<<<< HEAD let newLine: IBufferLine; const useRecycling = this.options.experimentalPushRecycling; if (useRecycling) { newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols) { - newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); + if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { + newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); this._blankLine = newLine; } newLine.isWrapped = !!(isWrapped); } else { - newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); + newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); } -======= - const newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); ->>>>>>> master const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1206,9 +1202,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { if (useRecycling) { - // this.buffer.lines.pushRecycling((item) => (item) ? item.copyFrom(newLine) : newLine.clone()); if (willBufferBeTrimmed) { - (this.buffer.lines as any).trimAndRecycle().copyFrom(newLine); + // Warning: Never call .trimAndRecycle() without the + // willBufferBeTrimmed guard! + this.buffer.lines.trimAndRecycle().copyFrom(newLine); } else { this.buffer.lines.push(newLine.clone()); } diff --git a/src/common/CircularList.test.ts b/src/common/CircularList.test.ts index 4c07b16c..5f0419ab 100644 --- a/src/common/CircularList.test.ts +++ b/src/common/CircularList.test.ts @@ -257,4 +257,20 @@ describe('CircularList', () => { assert.equal(list.get(3), 4); }); }); + describe('trimAndRecycle', function(): void { + it('should return correct element', function(): void { + const list = new CircularList(5); + list.push([0]); + list.push([1]); + list.push([2]); + list.push([3]); + list.push([4]); + assert.equal(list.trimAndRecycle()[0], 0); + assert.equal(list.trimAndRecycle()[0], 1); + assert.equal(list.trimAndRecycle()[0], 2); + assert.equal(list.trimAndRecycle()[0], 3); + assert.equal(list.trimAndRecycle()[0], 4); + assert.equal(list.trimAndRecycle()[0], 0); + }); + }); }); diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 48394c85..345eb106 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -101,23 +101,14 @@ export class CircularList extends EventEmitter implements ICircularList { } /** - * Recycling push variant with a callback. - * The callback gets the value at the current position to be overwritten. - * Return the new value from the callback. + * Recycling trim. + * This is used to recycle buffer lines in Terminal.scroll when + * the list is at maxLength as a push replacement. + * Returns the old line as new one to be recycled. + * Note: There are no bound checks for performance reasons, + * the method is a special optimization for Terminal.scroll, + * do not use it anywhere else. */ - public pushRecycling(callback: (item: T | undefined) => T): void { - this._array[this._getCyclicIndex(this._length)] = callback(this._array[this._getCyclicIndex(this._length)]); - if (this._length === this._maxLength) { - this._startIndex++; - if (this._startIndex === this._maxLength) { - this._startIndex = 0; - } - this.emit('trim', 1); - } else { - this._length++; - } - } - public trimAndRecycle(): T | undefined { this._startIndex = ++this._startIndex % this._maxLength; this.emit('trim', 1); diff --git a/src/common/Types.ts b/src/common/Types.ts index ec6fcfd5..a1c12adf 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -28,7 +28,7 @@ export interface ICircularList extends IEventEmitter { get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; - pushRecycling(callback: (item: T | undefined) => T): void; + trimAndRecycle(): T | undefined; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; From 793516060fe95dc305c16799dd73d243e8b75e74 Mon Sep 17 00:00:00 2001 From: leomoty Date: Sun, 28 Oct 2018 14:24:08 -0300 Subject: [PATCH 32/64] Recreate mouseHelper after renderer is disposed --- src/Terminal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index 63315008..8a837337 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -473,6 +473,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this._theme) { this.renderer.setTheme(this._theme); } + this.mouseHelper = new MouseHelper(this.renderer); break; case 'scrollback': this.buffers.resize(this.cols, this.rows); From 41b1c929e0d860100c9dca2f39aa0c08ac04a361 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 28 Oct 2018 10:50:16 -0700 Subject: [PATCH 33/64] Fix screen element remaining large if originally using canvas --- src/renderer/dom/DomRenderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index d55d2416..22ecd0cd 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -131,6 +131,8 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height; this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`; this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`; + this._terminal.screenElement.style.width = ''; + this._terminal.screenElement.style.height = ''; } public setTheme(theme: ITheme | undefined): IColorSet { From 108438dbebd053df4cc01e3a74174534a3a291d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 28 Oct 2018 10:51:57 -0700 Subject: [PATCH 34/64] Use setRenderer instead of ctor Just in case something hangs onto MouseHelper --- src/Terminal.ts | 2 +- src/utils/MouseHelper.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 8a837337..e8871462 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -473,7 +473,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this._theme) { this.renderer.setTheme(this._theme); } - this.mouseHelper = new MouseHelper(this.renderer); + this.mouseHelper.setRenderer(this.renderer); break; case 'scrollback': this.buffers.resize(this.cols, this.rows); diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts index 15f05742..ca1bb27e 100644 --- a/src/utils/MouseHelper.ts +++ b/src/utils/MouseHelper.ts @@ -9,6 +9,10 @@ import { IRenderer } from '../renderer/Types'; export class MouseHelper { constructor(private _renderer: IRenderer) {} + public setRenderer(renderer: IRenderer): void { + this._renderer = renderer; + } + public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] { // Ignore browsers that don't support MouseEvent.pageX if (event.pageX === null || event.pageX === undefined) { From fb64c527a58e9c1ba84c2ed46516010c77b320e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 30 Oct 2018 17:40:11 +0100 Subject: [PATCH 35/64] revert to precheck pushWouldTrim --- src/BufferLine.ts | 6 ++---- src/Terminal.ts | 18 ++++++++++-------- src/Types.ts | 2 +- src/common/CircularList.test.ts | 16 ---------------- src/common/CircularList.ts | 20 +++++--------------- src/common/Types.ts | 2 +- typings/xterm.d.ts | 7 ++++++- 7 files changed, 25 insertions(+), 46 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index c1ec2609..7c697334 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -94,11 +94,10 @@ export class BufferLine implements IBufferLine { } } - public copyFrom(line: BufferLine): IBufferLine { + public copyFrom(line: BufferLine): void { this._data = line._data.slice(0); this.length = line.length; this.isWrapped = line.isWrapped; - return this; } public clone(): IBufferLine { @@ -249,7 +248,7 @@ export class BufferLineTypedArray implements IBufferLine { } /** alter to a full copy of line */ - public copyFrom(line: BufferLineTypedArray): IBufferLine { + public copyFrom(line: BufferLineTypedArray): void { if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { @@ -262,7 +261,6 @@ export class BufferLineTypedArray implements IBufferLine { this._combined[el] = line._combined[el]; } this.isWrapped = line.isWrapped; - return this; } /** create a new clone */ diff --git a/src/Terminal.ts b/src/Terminal.ts index df73ca58..420db146 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -107,7 +107,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { rightClickSelectsWord: Browser.isMac, rendererType: 'canvas', experimentalBufferLineImpl: 'JsArray', - experimentalPushRecycling: false + experimentalBufferLineRecycling: false }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -1179,16 +1179,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Scroll the terminal down 1 row, creating a blank line. * @param isWrapped Whether the new line is wrapped from the previous line. */ - public scroll(isWrapped?: boolean): void { + public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; - const useRecycling = this.options.experimentalPushRecycling; + const useRecycling = this.options.experimentalBufferLineRecycling; if (useRecycling) { newLine = this._blankLine; if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); this._blankLine = newLine; } - newLine.isWrapped = !!(isWrapped); + newLine.isWrapped = isWrapped; } else { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); } @@ -1198,15 +1198,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.buffer.scrollTop === 0) { // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength; + const willBufferBeTrimmed = this.buffer.lines.pushWouldTrim(); // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { if (useRecycling) { if (willBufferBeTrimmed) { - // Warning: Never call .trimAndRecycle() without the - // willBufferBeTrimmed guard! - this.buffer.lines.trimAndRecycle().copyFrom(newLine); + // push would trim the oldest line in the ringbuffer + // therefore we can recycle it here as the new line + const recycled = this.buffer.lines.get(0); + recycled.copyFrom(newLine); + this.buffer.lines.push(recycled); } else { this.buffer.lines.push(newLine.clone()); } diff --git a/src/Types.ts b/src/Types.ts index cafe60db..e8578426 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -522,7 +522,7 @@ export interface IBufferLine { replaceCells(start: number, end: number, fill: CharData): void; resize(cols: number, fill: CharData, shrink?: boolean): void; fill(fillCharData: CharData): void; - copyFrom(line: IBufferLine): IBufferLine; + copyFrom(line: IBufferLine): void; clone(): IBufferLine; } diff --git a/src/common/CircularList.test.ts b/src/common/CircularList.test.ts index 5f0419ab..4c07b16c 100644 --- a/src/common/CircularList.test.ts +++ b/src/common/CircularList.test.ts @@ -257,20 +257,4 @@ describe('CircularList', () => { assert.equal(list.get(3), 4); }); }); - describe('trimAndRecycle', function(): void { - it('should return correct element', function(): void { - const list = new CircularList(5); - list.push([0]); - list.push([1]); - list.push([2]); - list.push([3]); - list.push([4]); - assert.equal(list.trimAndRecycle()[0], 0); - assert.equal(list.trimAndRecycle()[0], 1); - assert.equal(list.trimAndRecycle()[0], 2); - assert.equal(list.trimAndRecycle()[0], 3); - assert.equal(list.trimAndRecycle()[0], 4); - assert.equal(list.trimAndRecycle()[0], 0); - }); - }); }); diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 345eb106..00d5a520 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -90,10 +90,7 @@ export class CircularList extends EventEmitter implements ICircularList { public push(value: T): void { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { - this._startIndex++; - if (this._startIndex === this._maxLength) { - this._startIndex = 0; - } + this._startIndex = ++this._startIndex % this._maxLength; this.emit('trim', 1); } else { this._length++; @@ -101,18 +98,11 @@ export class CircularList extends EventEmitter implements ICircularList { } /** - * Recycling trim. - * This is used to recycle buffer lines in Terminal.scroll when - * the list is at maxLength as a push replacement. - * Returns the old line as new one to be recycled. - * Note: There are no bound checks for performance reasons, - * the method is a special optimization for Terminal.scroll, - * do not use it anywhere else. + * Whether a push would trim. + * True when the ringbuffer is full. */ - public trimAndRecycle(): T | undefined { - this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); - return this._array[this._getCyclicIndex(this._length - 1)]; + public pushWouldTrim(): boolean { + return this._length === this._maxLength; } /** diff --git a/src/common/Types.ts b/src/common/Types.ts index a1c12adf..c866581d 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -28,7 +28,7 @@ export interface ICircularList extends IEventEmitter { get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; - trimAndRecycle(): T | undefined; + pushWouldTrim(): boolean; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 68e1e188..8b36dd75 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -112,7 +112,12 @@ declare module 'xterm' { */ experimentalBufferLineImpl?: 'JsArray' | 'TypedArray'; - experimentalPushRecycling?: boolean; + /** + * (EXPERIMENTAL) Enable recycling of buffer lines. + * + * This option will be removed in the future. + */ + experimentalBufferLineRecycling?: boolean; /** * The font size used to render text. From 160b4a7d669b6c527fec1bc40a18ae79db7d19de Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 30 Oct 2018 11:04:32 -0700 Subject: [PATCH 36/64] Revert "Remove request term info handler" This reverts commit c2994b0ae1b72fffe95312ef74c5d32897ac82a7. --- src/InputHandler.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 091f41cf..a34590ef 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -24,6 +24,26 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * DCS subparser implementations */ + /** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not supported + */ +class RequestTerminfo implements IDcsHandler { + private _data: string; + constructor(private _terminal: any) { } + hook(collect: string, params: number[], flag: number): void { + this._data = ''; + } + put(data: string, start: number, end: number): void { + this._data += data.substring(start, end); + } + unhook(): void { + // invalid: DCS 0 + r Pt ST + this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`); + } +} + /** * DCS $ q Pt ST * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) @@ -66,7 +86,7 @@ class DECRQSS implements IDcsHandler { default: // invalid: DCS 0 $ r Pt ST (xterm) this._terminal.error('Unknown DCS $q %s', this._data); - this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`); + this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`); } } } @@ -267,6 +287,7 @@ export class InputHandler extends Disposable implements IInputHandler { * DCS handler */ this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); + this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); } public dispose(): void { From e9906f9b9e73f2a8ffefcb6ceccb8dd4c20bd864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 7 Nov 2018 00:56:45 +0100 Subject: [PATCH 37/64] compromise between code safety and speed --- src/Terminal.ts | 8 ++------ src/common/CircularList.ts | 20 +++++++++++++++++--- src/common/Types.ts | 3 ++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 420db146..13be90ef 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1198,17 +1198,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.buffer.scrollTop === 0) { // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = this.buffer.lines.pushWouldTrim(); + const willBufferBeTrimmed = this.buffer.lines.isFull(); // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { if (useRecycling) { if (willBufferBeTrimmed) { - // push would trim the oldest line in the ringbuffer - // therefore we can recycle it here as the new line - const recycled = this.buffer.lines.get(0); - recycled.copyFrom(newLine); - this.buffer.lines.push(recycled); + this.buffer.lines.recycle().copyFrom(newLine); } else { this.buffer.lines.push(newLine.clone()); } diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 00d5a520..0a57ece0 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -98,10 +98,24 @@ export class CircularList extends EventEmitter implements ICircularList { } /** - * Whether a push would trim. - * True when the ringbuffer is full. + * Advance ringbuffer index and return current element for recycling. + * Note: If the ringbuffer is not full this method will return undefined, + * Either precheck with isFull() or handle the undefined return value accordingly. */ - public pushWouldTrim(): boolean { + public recycle(): T | undefined { + if (this._length === this._maxLength) { + this._startIndex = ++this._startIndex % this._maxLength; + this.emit('trim', 1); + } else { + this._length++; + } + return this._array[this._getCyclicIndex(this._length - 1)]; + } + + /** + * Ringbuffer is at max length. + */ + public isFull(): boolean { return this._length === this._maxLength; } diff --git a/src/common/Types.ts b/src/common/Types.ts index c866581d..841029ec 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -28,7 +28,8 @@ export interface ICircularList extends IEventEmitter { get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; - pushWouldTrim(): boolean; + recycle(): T | undefined; + isFull(): boolean; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; From 8acf137d148d430bcbbb2f8627a3726abe4fe1a0 Mon Sep 17 00:00:00 2001 From: Antonin Stefanutti Date: Thu, 8 Nov 2018 14:44:18 +0100 Subject: [PATCH 38/64] Use WheelEvent.deltaY instead of WheelEvent.wheelDeltaY for wheel event Fixes #1781 --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index e8871462..c9bc98ff 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -985,7 +985,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II : 65; break; case 'wheel': - button = (ev).wheelDeltaY > 0 + button = (ev).deltaY < 0 ? 64 : 65; break; From de7d5e1eecc4398ef8e7e5db0662002631a6508b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 9 Nov 2018 01:03:44 +0100 Subject: [PATCH 39/64] swallow 7bit ST, add missing executables --- src/EscapeSequenceParser.test.ts | 12 ++++++++++-- src/EscapeSequenceParser.ts | 5 ++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 5db6d7ff..e92f4125 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -369,7 +369,8 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; parser.parse(collect[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['esc', '', collect[i]]]); + // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled + testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); parser.reset(); testTerminal.clear(); } @@ -1051,6 +1052,13 @@ describe('EscapeSequenceParser', function (): void { ['csi', '<', [0, 0], 'c'] ], null); }); + it('7bit ST should be swallowed', function(): void { + test('abc\x9d123tzf\x1b\\defg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); }); }); @@ -1089,7 +1097,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.GROUND; - parser.parse('\x1e'); + parser.parse('\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index bf744e05..b38c50f5 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -66,7 +66,7 @@ export class TransitionTable { const PRINTABLES = r(0x20, 0x7f); const EXECUTABLES = r(0x00, 0x18); EXECUTABLES.push(0x19); -EXECUTABLES.concat(r(0x1c, 0x20)); +EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND; /** @@ -261,6 +261,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsHandlers = Object.create(null); this._activeDcsHandler = null; this._errorHandler = this._errorHandlerFb; + + // swallow 7bit ST (ESC+\) + this.setEscHandler('\\', () => {}); } public dispose(): void { From 774793c7ca77093bb9878d654159164ca28aae98 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 8 Nov 2018 19:20:12 -0800 Subject: [PATCH 40/64] Fixing isClickInSelection to cover missing cases The following cases weren't working: - The first character on single line selections - The first row on multi-line selection --- src/SelectionManager.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index bfb57177..47cad7ef 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -290,8 +290,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } return (coords[1] > start[1] && coords[1] < end[1]) || - (start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) || - (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]); + (start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]); } /** From 8ea0bb55b4d2f0e2e1fbbaf79ddaee72938bc1cd Mon Sep 17 00:00:00 2001 From: Jeff Smith Date: Thu, 8 Nov 2018 00:11:50 -0600 Subject: [PATCH 41/64] Make scrollbar draggable when using DOM renderer Fixes #1751 --- src/renderer/dom/DomRenderer.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 22ecd0cd..fadd9b72 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -123,16 +123,14 @@ export class DomRenderer extends EventEmitter implements IRenderer { ` display: inline-block;` + ` height: 100%;` + ` vertical-align: top;` + - ` width: ${this._terminal.charMeasure.width}px` + + ` width: ${this.dimensions.actualCellWidth}px` + `}`; this._dimensionsStyleElement.innerHTML = styles; this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height; - this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`; - this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`; - this._terminal.screenElement.style.width = ''; - this._terminal.screenElement.style.height = ''; + this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public setTheme(theme: ITheme | undefined): IColorSet { From 9faebaf5fe8f7e480c4206a041fc2b67679fb3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 15 Nov 2018 00:16:52 +0100 Subject: [PATCH 42/64] hold single AudioContext instance --- src/SoundManager.ts | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/SoundManager.ts b/src/SoundManager.ts index 4139c207..ffb01658 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -12,7 +12,19 @@ import { ITerminal, ISoundManager } from './Types'; export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; export class SoundManager implements ISoundManager { - private _audioContext: AudioContext; + private static _audioContext: AudioContext; + + static get audioContext(): AudioContext { + if (!SoundManager._audioContext) { + const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; + if (!audioContextCtor) { + console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); + return null; + } + this._audioContext = new audioContextCtor(); + } + return SoundManager._audioContext; + } constructor( private _terminal: ITerminal @@ -20,22 +32,16 @@ export class SoundManager implements ISoundManager { } public playBellSound(): void { - const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; - if (!this._audioContext && audioContextCtor) { - this._audioContext = new audioContextCtor(); - } - - if (this._audioContext) { - const bellAudioSource = this._audioContext.createBufferSource(); - const context = this._audioContext; - this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { - bellAudioSource.buffer = buffer; - bellAudioSource.connect(context.destination); - bellAudioSource.start(0); - }); - } else { - console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version'); + const context = SoundManager.audioContext; + if (!context) { + return; } + const bellAudioSource = context.createBufferSource(); + context.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { + bellAudioSource.buffer = buffer; + bellAudioSource.connect(context.destination); + bellAudioSource.start(0); + }); } private _base64ToArrayBuffer(base64: string): ArrayBuffer { From e3146961bef54bb8e44206c90e5901c6d76434fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 15 Nov 2018 00:25:23 +0100 Subject: [PATCH 43/64] make linter happy --- src/SoundManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SoundManager.ts b/src/SoundManager.ts index ffb01658..a31c7574 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -13,7 +13,7 @@ export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBA export class SoundManager implements ISoundManager { private static _audioContext: AudioContext; - + static get audioContext(): AudioContext { if (!SoundManager._audioContext) { const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; From 0e3f9b72d1725f1797992b04e5ce320b69c56e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 15 Nov 2018 00:48:31 +0100 Subject: [PATCH 44/64] allow to set audio context per argument --- src/SoundManager.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/SoundManager.ts b/src/SoundManager.ts index a31c7574..0ba6e0c2 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -14,7 +14,7 @@ export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBA export class SoundManager implements ISoundManager { private static _audioContext: AudioContext; - static get audioContext(): AudioContext { + static get audioContext(): AudioContext | null { if (!SoundManager._audioContext) { const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; if (!audioContextCtor) { @@ -27,19 +27,20 @@ export class SoundManager implements ISoundManager { } constructor( - private _terminal: ITerminal + private _terminal: ITerminal, + private _audioContext?: AudioContext ) { } public playBellSound(): void { - const context = SoundManager.audioContext; - if (!context) { + const ctx = this._audioContext || SoundManager.audioContext; + if (!ctx) { return; } - const bellAudioSource = context.createBufferSource(); - context.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { + const bellAudioSource = ctx.createBufferSource(); + ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { bellAudioSource.buffer = buffer; - bellAudioSource.connect(context.destination); + bellAudioSource.connect(ctx.destination); bellAudioSource.start(0); }); } From a480f483cda4d73edd1f2963a1ae61e32acd973c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Nov 2018 17:54:03 -0800 Subject: [PATCH 45/64] Add tests --- src/SelectionManager.test.ts | 13 +++++++++++++ src/SelectionManager.ts | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c42735d5..8735e894 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -30,6 +30,7 @@ class TestSelectionManager extends SelectionManager { public selectLineAt(line: number): void { this._selectLineAt(line); } public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } + public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); } // Disable DOM interaction public enable(): void {} @@ -478,5 +479,17 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'a\n😁\nc'); }); }); + + describe('_areCoordsInSelection', () => { + it('should return whether coords are in the selection', () => { + assert.isFalse(selectionManager.areCoordsInSelection([0, 0], [2, 0], [2, 1])); + assert.isFalse(selectionManager.areCoordsInSelection([1, 0], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([2, 0], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([10, 0], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([0, 1], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([1, 1], [2, 0], [2, 1])); + assert.isFalse(selectionManager.areCoordsInSelection([2, 1], [2, 0], [2, 1])); + }); + }); }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 47cad7ef..86be0c48 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -289,6 +289,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager return false; } + return this._areCoordsInSelection(coords, start, end); + } + + protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return (coords[1] > start[1] && coords[1] < end[1]) || (start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) || (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) || From 203ee0f644f833d2dd554a147ec3274349272887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 18 Nov 2018 17:47:49 +0100 Subject: [PATCH 46/64] remove ctx args from ctor --- src/SoundManager.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SoundManager.ts b/src/SoundManager.ts index 0ba6e0c2..9adbfc4a 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -27,13 +27,12 @@ export class SoundManager implements ISoundManager { } constructor( - private _terminal: ITerminal, - private _audioContext?: AudioContext + private _terminal: ITerminal ) { } public playBellSound(): void { - const ctx = this._audioContext || SoundManager.audioContext; + const ctx = SoundManager.audioContext; if (!ctx) { return; } From 9286ea3eafe61c3ea9170378568d5fe6f46809f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 18 Nov 2018 17:50:29 +0100 Subject: [PATCH 47/64] make statix usage explicit --- src/SoundManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SoundManager.ts b/src/SoundManager.ts index 9adbfc4a..6084edcb 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -21,7 +21,7 @@ export class SoundManager implements ISoundManager { console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); return null; } - this._audioContext = new audioContextCtor(); + SoundManager._audioContext = new audioContextCtor(); } return SoundManager._audioContext; } From 9b762854eb2626d420eeb58090332bc7a9f8a9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 18 Nov 2018 23:31:45 +0100 Subject: [PATCH 48/64] faster wcwidth init and runtime; fix slightly wrong surrogate handling --- src/CharWidth.ts | 87 +++++++++++++++++++++++---------------------- src/InputHandler.ts | 41 +++++++++++---------- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/CharWidth.ts b/src/CharWidth.ts index d54e9392..045a796c 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -120,50 +120,41 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu return 1; } const control = opts.control | 0; - let table: number[] | Uint32Array = null; - function initTable(): number[] | Uint32Array { - // lookup table for BMP - const CODEPOINTS = 65536; // BMP holds 65536 codepoints - const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 - const ITEMSIZE = 32; // using uint32_t - const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE; - const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH; - table = (typeof Uint32Array === 'undefined') - ? new Array(CONTAINERSIZE) - : new Uint32Array(CONTAINERSIZE); - for (let i = 0; i < CONTAINERSIZE; ++i) { - let num = 0; - let pos = CODEPOINTS_PER_ITEM; - while (pos--) { - num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); - } - table[i] = num; - } - return table; + + // create lookup table for BMP plane + // TODO: make callable/configurable from UnicodeManager + const table = new Uint8Array(65536); + table.fill(1); + table[0] = opts.nul; + // control chars + table.subarray(1, 32).fill(opts.control); + table.subarray(0x7f, 0xa0).fill(opts.control); + // combining 0 + for (let r = 0; r < COMBINING_BMP.length; ++r) { + table.subarray(COMBINING_BMP[r][0], COMBINING_BMP[r][1]).fill(0); } - // get width from lookup table - // position in container : num / CODEPOINTS_PER_ITEM - // ==> n = table[Math.floor(num / 16)] - // ==> n = table[num >> 4] - // 16 codepoints per number: FFEEDDCCBBAA99887766554433221100 - // position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH - // ==> m = (n % 16) * 2 - // ==> m = (num & 15) << 1 - // right shift to position m - // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 - // we are only interested in 2 LSBs, cut off higher bits - // ==> n = n & 3 e.g. 000000000000000000000000000000XX + // wide chars + table.subarray(0x1100, 0x1160).fill(2); + table[0x2329] = 2; + table[0x232a] = 2; + table.subarray(0x2e80, 0xa4d0).fill(2); + table[0x303f] = 1; // wrongly added before + table.subarray(0xac00, 0xd7a4).fill(2); + table.subarray(0xf900, 0xfb00).fill(2); + table.subarray(0xfe10, 0xfe1a).fill(2); + table.subarray(0xfe30, 0xfe70).fill(2); + table.subarray(0xff00, 0xff61).fill(2); + table.subarray(0xffe0, 0xffe7).fill(2); + return function (num: number): number { - num = num | 0; // get asm.js like optimization under V8 if (num < 32) { return control | 0; } if (num < 127) { return 1; } - const t = table || initTable(); if (num < 65536) { - return t[num >> 4] >> ((num & 15) << 1) & 3; + return table[num]; } // do a full search for high codepoints return wcwidthHigh(num); @@ -175,17 +166,27 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu */ export function getStringCellWidth(s: string): number { let result = 0; - for (let i = 0; i < s.length; ++i) { + const length = s.length; + for (let i = 0; i < length; ++i) { let code = s.charCodeAt(i); + // surrogate pair first if (0xD800 <= code && code <= 0xDBFF) { - const low = s.charCodeAt(i + 1); - if (isNaN(low)) { - return result; + if (++i >= length) { + // this should not happen with strings retrieved from + // Buffer.translateToString as it converts from UTF-32 + // and therefore always should contain the second part + // for any other string we still have to handle it somehow: + // simply treat the lonely surrogate first as a single char (UCS-2 behavior) + return result + wcwidth(code); + } + const second = s.charCodeAt(i); + // convert surrogate pair to high codepoint only for valid second part (UTF-16) + // otherwise treat them independently (UCS-2 behavior) + if (0xDC00 <= second && second <= 0xDFFF) { + code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + result += wcwidth(second); } - code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - } - if (0xDC00 <= code && code <= 0xDFFF) { - continue; } result += wcwidth(code); } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a34590ef..893046bf 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -113,7 +113,7 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _surrogateHigh: string; + private _surrogateFirst: string; constructor( protected _terminal: IInputHandlingTerminal, @@ -123,7 +123,7 @@ export class InputHandler extends Disposable implements IInputHandler { this.register(this._parser); - this._surrogateHigh = ''; + this._surrogateFirst = ''; /** * custom fallback handlers @@ -311,9 +311,9 @@ export class InputHandler extends Disposable implements IInputHandler { } // apply leftover surrogate high from last write - if (this._surrogateHigh) { - data = this._surrogateHigh + data; - this._surrogateHigh = ''; + if (this._surrogateFirst) { + data = this._surrogateFirst + data; + this._surrogateFirst = ''; } this._parser.parse(data); @@ -327,7 +327,7 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: string, start: number, end: number): void { let char: string; let code: number; - let low: number; + let second: number; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -345,20 +345,25 @@ export class InputHandler extends Disposable implements IInputHandler { // surrogate pair handling if (0xD800 <= code && code <= 0xDBFF) { - // we got a surrogate high - // get surrogate low (next 2 bytes) - low = data.charCodeAt(stringPosition + 1); - if (isNaN(low)) { - // end of data stream, save surrogate high - this._surrogateHigh = char; + if (++stringPosition >= end) { + // end of input: + // handle pairs as true UTF-16 and wait for the second part + // since we expect the input comming from a stream there is + // a small chance that the surrogate pair got split + // therefore we dont process the first char here, instead + // it gets added as first char to the next processed chunk + this._surrogateFirst = char; continue; } - code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - char += data.charAt(stringPosition + 1); - } - // surrogate low - already handled above - if (0xDC00 <= code && code <= 0xDFFF) { - continue; + second = data.charCodeAt(stringPosition); + // if the second part is in surrogate pair range create the high codepoint + // otherwise fall back to UCS-2 behavior (handle codepoints independently) + if (0xDC00 <= second && second <= 0xDFFF) { + code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + char += data.charAt(stringPosition); + } else { + stringPosition--; + } } // calculate print space From 21f848d0ab21205dfe26f94ca0753ab461e3f591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 18 Nov 2018 23:48:59 +0100 Subject: [PATCH 49/64] regression test --- src/CharWidth.test.ts | 178 +++++++++++++++++++++++++++++++++++++++++- src/CharWidth.ts | 50 +++--------- 2 files changed, 190 insertions(+), 38 deletions(-) diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index 3242dd77..c2d10c5c 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -5,7 +5,7 @@ import { TestTerminal } from './utils/TestUtils.test'; import { assert } from 'chai'; -import { getStringCellWidth } from './CharWidth'; +import { getStringCellWidth, wcwidth } from './CharWidth'; import { IBuffer } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; @@ -77,3 +77,179 @@ describe('getStringCellWidth', function(): void { }); // TODO: multiline tests once #1685 is resolved }); + +describe('wcwidth regression', function(): void { + // TODO: remove with one of the next releases + const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number { + // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c + // combining characters + const COMBINING_BMP = [ + [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], + [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], + [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], + [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], + [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], + [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], + [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], + [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], + [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], + [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], + [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], + [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], + [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], + [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], + [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], + [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], + [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], + [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], + [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], + [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], + [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], + [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], + [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], + [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], + [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], + [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], + [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], + [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], + [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], + [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], + [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], + [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], + [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], + [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], + [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], + [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], + [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], + [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], + [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], + [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], + [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], + [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] + ]; + const COMBINING_HIGH = [ + [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], + [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], + [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], + [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], + [0xE0100, 0xE01EF] + ]; + // binary search + function bisearch(ucs: number, data: number[][]): boolean { + let min = 0; + let max = data.length - 1; + let mid; + if (ucs < data[0][0] || ucs > data[max][1]) { + return false; + } + while (max >= min) { + mid = (min + max) >> 1; + if (ucs > data[mid][1]) { + min = mid + 1; + } else if (ucs < data[mid][0]) { + max = mid - 1; + } else { + return true; + } + } + return false; + } + function wcwidthBMP(ucs: number): number { + // test for 8-bit control characters + if (ucs === 0) { + return opts.nul; + } + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { + return opts.control; + } + // binary search in table of non-spacing characters + if (bisearch(ucs, COMBINING_BMP)) { + return 0; + } + // if we arrive here, ucs is not a combining or C0/C1 control character + if (isWideBMP(ucs)) { + return 2; + } + return 1; + } + function isWideBMP(ucs: number): boolean { + return ( + ucs >= 0x1100 && ( + ucs <= 0x115f || // Hangul Jamo init. consonants + ucs === 0x2329 || + ucs === 0x232a || + (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi + (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables + (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs + (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms + (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms + (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms + (ucs >= 0xffe0 && ucs <= 0xffe6))); + } + function wcwidthHigh(ucs: number): 0 | 1 | 2 { + if (bisearch(ucs, COMBINING_HIGH)) { + return 0; + } + if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { + return 2; + } + return 1; + } + const control = opts.control | 0; + let table: number[] | Uint32Array = null; + function initTable(): number[] | Uint32Array { + // lookup table for BMP + const CODEPOINTS = 65536; // BMP holds 65536 codepoints + const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 + const ITEMSIZE = 32; // using uint32_t + const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE; + const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH; + table = (typeof Uint32Array === 'undefined') + ? new Array(CONTAINERSIZE) + : new Uint32Array(CONTAINERSIZE); + for (let i = 0; i < CONTAINERSIZE; ++i) { + let num = 0; + let pos = CODEPOINTS_PER_ITEM; + while (pos--) { + num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); + } + table[i] = num; + } + return table; + } + // get width from lookup table + // position in container : num / CODEPOINTS_PER_ITEM + // ==> n = table[Math.floor(num / 16)] + // ==> n = table[num >> 4] + // 16 codepoints per number: FFEEDDCCBBAA99887766554433221100 + // position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH + // ==> m = (n % 16) * 2 + // ==> m = (num & 15) << 1 + // right shift to position m + // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 + // we are only interested in 2 LSBs, cut off higher bits + // ==> n = n & 3 e.g. 000000000000000000000000000000XX + return function (num: number): number { + num = num | 0; // get asm.js like optimization under V8 + if (num < 32) { + return control | 0; + } + if (num < 127) { + return 1; + } + const t = table || initTable(); + if (num < 65536) { + return t[num >> 4] >> ((num & 15) << 1) & 3; + } + // do a full search for high codepoints + return wcwidthHigh(num); + }; + })({nul: 0, control: 0}); // configurable options + + it('equality of old and new impl', function(): void { + for (let i = 0; i < 65536; ++i) { + assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`); + } + }); +}); diff --git a/src/CharWidth.ts b/src/CharWidth.ts index 045a796c..05fb5d37 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -78,38 +78,6 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu } return false; } - function wcwidthBMP(ucs: number): number { - // test for 8-bit control characters - if (ucs === 0) { - return opts.nul; - } - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { - return opts.control; - } - // binary search in table of non-spacing characters - if (bisearch(ucs, COMBINING_BMP)) { - return 0; - } - // if we arrive here, ucs is not a combining or C0/C1 control character - if (isWideBMP(ucs)) { - return 2; - } - return 1; - } - function isWideBMP(ucs: number): boolean { - return ( - ucs >= 0x1100 && ( - ucs <= 0x115f || // Hangul Jamo init. consonants - ucs === 0x2329 || - ucs === 0x232a || - (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi - (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables - (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs - (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms - (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms - (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms - (ucs >= 0xffe0 && ucs <= 0xffe6))); - } function wcwidthHigh(ucs: number): 0 | 1 | 2 { if (bisearch(ucs, COMBINING_HIGH)) { return 0; @@ -129,16 +97,15 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu // control chars table.subarray(1, 32).fill(opts.control); table.subarray(0x7f, 0xa0).fill(opts.control); - // combining 0 - for (let r = 0; r < COMBINING_BMP.length; ++r) { - table.subarray(COMBINING_BMP[r][0], COMBINING_BMP[r][1]).fill(0); - } + + // apply wide char rules first // wide chars table.subarray(0x1100, 0x1160).fill(2); table[0x2329] = 2; table[0x232a] = 2; table.subarray(0x2e80, 0xa4d0).fill(2); - table[0x303f] = 1; // wrongly added before + table[0x303f] = 1; // wrongly in last line + table.subarray(0xac00, 0xd7a4).fill(2); table.subarray(0xf900, 0xfb00).fill(2); table.subarray(0xfe10, 0xfe1a).fill(2); @@ -146,6 +113,15 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu table.subarray(0xff00, 0xff61).fill(2); table.subarray(0xffe0, 0xffe7).fill(2); + // apply combining last to ensure we overwrite + // wrongly wide set chars: + // the original algo evals combining first and falls + // through to wide check so we simply do here the opposite + // combining 0 + for (let r = 0; r < COMBINING_BMP.length; ++r) { + table.subarray(COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1).fill(0); + } + return function (num: number): number { if (num < 32) { return control | 0; From 006869704b41732cae36822b05e978c8884c29ad Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 20 Nov 2018 09:44:10 -0800 Subject: [PATCH 50/64] Add help wanted/good first issue links --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4b3a9f2..5c389f5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,8 @@ opening an issue, read these pointers. ## Contributing code -- Make sure you have a [GitHub account](https://github.com/join) +You can find issues to work on my looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: + - Fork [xterm.js](https://github.com/sourcelair/xterm.js/) ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) - Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running From 3964bb28b5c3fe061f3c4eba43c78635d4737c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 20 Nov 2018 21:49:31 +0100 Subject: [PATCH 51/64] throw error in recycle with buffer not full --- src/common/CircularList.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 0a57ece0..5a6a652f 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -99,17 +99,16 @@ export class CircularList extends EventEmitter implements ICircularList { /** * Advance ringbuffer index and return current element for recycling. - * Note: If the ringbuffer is not full this method will return undefined, - * Either precheck with isFull() or handle the undefined return value accordingly. + * Note: The buffer must be full for this method to work. + * @throws When the buffer is not full. */ - public recycle(): T | undefined { - if (this._length === this._maxLength) { - this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); - } else { - this._length++; + public recycle(): T { + if (this._length !== this._maxLength) { + throw new Error('Can only recycle when the buffer is full'); } - return this._array[this._getCyclicIndex(this._length - 1)]; + this._startIndex = ++this._startIndex % this._maxLength; + this.emit('trim', 1); + return this._array[this._getCyclicIndex(this._length - 1)]!; } /** From 15c4cc8a74c89a9aab02d5277ec66c39aa88664e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 20 Nov 2018 22:01:34 +0100 Subject: [PATCH 52/64] remove experimental flag for recycling, merge with TypedArray setting --- src/Terminal.ts | 7 +++---- src/common/CircularList.ts | 2 +- src/common/Types.ts | 2 +- typings/xterm.d.ts | 7 ------- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 70dc15d8..38eb3d5b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -106,8 +106,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { theme: null, rightClickSelectsWord: Browser.isMac, rendererType: 'canvas', - experimentalBufferLineImpl: 'JsArray', - experimentalBufferLineRecycling: false + experimentalBufferLineImpl: 'JsArray' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -1181,7 +1180,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; - const useRecycling = this.options.experimentalBufferLineRecycling; + const useRecycling = this.options.experimentalBufferLineImpl === 'TypedArray'; if (useRecycling) { newLine = this._blankLine; if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { @@ -1198,7 +1197,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.buffer.scrollTop === 0) { // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = this.buffer.lines.isFull(); + const willBufferBeTrimmed = this.buffer.lines.isFull; // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 5a6a652f..9faf534a 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -114,7 +114,7 @@ export class CircularList extends EventEmitter implements ICircularList { /** * Ringbuffer is at max length. */ - public isFull(): boolean { + public get isFull(): boolean { return this._length === this._maxLength; } diff --git a/src/common/Types.ts b/src/common/Types.ts index 841029ec..8a416bf1 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -24,12 +24,12 @@ export interface IKeyboardEvent { export interface ICircularList extends IEventEmitter { length: number; maxLength: number; + isFull: boolean; get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; recycle(): T | undefined; - isFull(): boolean; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 8b36dd75..c6b6b1e5 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -112,13 +112,6 @@ declare module 'xterm' { */ experimentalBufferLineImpl?: 'JsArray' | 'TypedArray'; - /** - * (EXPERIMENTAL) Enable recycling of buffer lines. - * - * This option will be removed in the future. - */ - experimentalBufferLineRecycling?: boolean; - /** * The font size used to render text. */ From 36db03e7133676e4044c94084bd4b7f9ebe8cab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 20 Nov 2018 22:35:51 +0100 Subject: [PATCH 53/64] keep old impl in test as reference; minor changes --- src/CharWidth.test.ts | 13 ++++++------- src/CharWidth.ts | 1 - src/InputHandler.ts | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index c2d10c5c..d4ddd24c 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -78,8 +78,8 @@ describe('getStringCellWidth', function(): void { // TODO: multiline tests once #1685 is resolved }); -describe('wcwidth regression', function(): void { - // TODO: remove with one of the next releases +it('wcwidth should match all values from the old implementation', function(): void { + // old implementation const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number { // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c // combining characters @@ -247,9 +247,8 @@ describe('wcwidth regression', function(): void { }; })({nul: 0, control: 0}); // configurable options - it('equality of old and new impl', function(): void { - for (let i = 0; i < 65536; ++i) { - assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`); - } - }); + // test full BMP range old vs new implmenetation + for (let i = 0; i < 65536; ++i) { + assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`); + } }); diff --git a/src/CharWidth.ts b/src/CharWidth.ts index 05fb5d37..d099b109 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -90,7 +90,6 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu const control = opts.control | 0; // create lookup table for BMP plane - // TODO: make callable/configurable from UnicodeManager const table = new Uint8Array(65536); table.fill(1); table[0] = opts.nul; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 893046bf..b4ce054f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -327,7 +327,6 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: string, start: number, end: number): void { let char: string; let code: number; - let second: number; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -355,7 +354,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._surrogateFirst = char; continue; } - second = data.charCodeAt(stringPosition); + const second = data.charCodeAt(stringPosition); // if the second part is in surrogate pair range create the high codepoint // otherwise fall back to UCS-2 behavior (handle codepoints independently) if (0xDC00 <= second && second <= 0xDFFF) { From ef6fb93a928061bb2ef26563e76f0ed7298793fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 21 Nov 2018 00:07:28 +0100 Subject: [PATCH 54/64] polyfill for TypedArray.fill --- src/CharWidth.ts | 26 ++--- src/core/TypedArrayUtils.test.ts | 157 +++++++++++++++++++++++++++++++ src/core/TypedArrayUtils.ts | 40 ++++++++ 3 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 src/core/TypedArrayUtils.test.ts create mode 100644 src/core/TypedArrayUtils.ts diff --git a/src/CharWidth.ts b/src/CharWidth.ts index d099b109..341b347a 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -1,3 +1,5 @@ +import { fill } from './core/TypedArrayUtils'; + /** * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT @@ -91,26 +93,26 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu // create lookup table for BMP plane const table = new Uint8Array(65536); - table.fill(1); + fill(table, 1); table[0] = opts.nul; // control chars - table.subarray(1, 32).fill(opts.control); - table.subarray(0x7f, 0xa0).fill(opts.control); + fill(table, opts.control, 1, 32); + fill(table, opts.control, 0x7f, 0xa0); // apply wide char rules first // wide chars - table.subarray(0x1100, 0x1160).fill(2); + fill(table, 2, 0x1100, 0x1160); table[0x2329] = 2; table[0x232a] = 2; - table.subarray(0x2e80, 0xa4d0).fill(2); + fill(table, 2, 0x2e80, 0xa4d0); table[0x303f] = 1; // wrongly in last line - table.subarray(0xac00, 0xd7a4).fill(2); - table.subarray(0xf900, 0xfb00).fill(2); - table.subarray(0xfe10, 0xfe1a).fill(2); - table.subarray(0xfe30, 0xfe70).fill(2); - table.subarray(0xff00, 0xff61).fill(2); - table.subarray(0xffe0, 0xffe7).fill(2); + fill(table, 2, 0xac00, 0xd7a4); + fill(table, 2, 0xf900, 0xfb00); + fill(table, 2, 0xfe10, 0xfe1a); + fill(table, 2, 0xfe30, 0xfe70); + fill(table, 2, 0xff00, 0xff61); + fill(table, 2, 0xffe0, 0xffe7); // apply combining last to ensure we overwrite // wrongly wide set chars: @@ -118,7 +120,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu // through to wide check so we simply do here the opposite // combining 0 for (let r = 0; r < COMBINING_BMP.length; ++r) { - table.subarray(COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1).fill(0); + fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1); } return function (num: number): number { diff --git a/src/core/TypedArrayUtils.test.ts b/src/core/TypedArrayUtils.test.ts new file mode 100644 index 00000000..2e45d371 --- /dev/null +++ b/src/core/TypedArrayUtils.test.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { fill } from './TypedArrayUtils'; + +type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + +function loopFill(array: TypedArray, value: number, start: number = 0, end?: number | undefined): TypedArray { + if (start >= array.length) { + return array; + } + start = (array.length + start) % array.length; + if (end === undefined) { + end = array.length; + } + if (end >= array.length) { + end = array.length; + } else { + end = (array.length + end) % array.length; + } + for (let i = start; i < end; ++i) { + array[i] = value; + } + return array; +} + +function deepEquals(a: TypedArray, b: TypedArray) { + assert.equal(a.length, b.length); + for (let i = 0; i < a.length; ++i) { + assert.equal(a[i], b[i]); + } +} + + +describe('polyfill conformance tests', function(): void { + describe('TypedArray.fill', function(): void { + it('should work with all typed array types', function(): void { + const u8_1 = new Uint8Array(5); + const u8_2 = new Uint8Array(5); + deepEquals(fill(u8_1, 2), u8_2.fill(2)); + deepEquals(fill(u8_1, -1), u8_2.fill(-1)); + const u16_1 = new Uint16Array(5); + const u16_2 = new Uint16Array(5); + deepEquals(fill(u16_1, 2), u16_2.fill(2)); + deepEquals(fill(u16_1, 65535), u16_2.fill(65535)); + deepEquals(fill(u16_1, -1), u16_2.fill(-1)); + const u32_1 = new Uint32Array(5); + const u32_2 = new Uint32Array(5); + deepEquals(fill(u32_1, 2), u32_2.fill(2)); + deepEquals(fill(u32_1, 65537), u32_2.fill(65537)); + deepEquals(fill(u32_1, -1), u32_2.fill(-1)); + const i8_1 = new Int8Array(5); + const i8_2 = new Int8Array(5); + deepEquals(fill(i8_1, 2), i8_2.fill(2)); + deepEquals(fill(i8_1, -1), i8_2.fill(-1)); + const i16_1 = new Int16Array(5); + const i16_2 = new Int16Array(5); + deepEquals(fill(i16_1, 2), i16_2.fill(2)); + deepEquals(fill(i16_1, 65535), i16_2.fill(65535)); + deepEquals(fill(i16_1, -1), i16_2.fill(-1)); + const i32_1 = new Int32Array(5); + const i32_2 = new Int32Array(5); + deepEquals(fill(i32_1, 2), i32_2.fill(2)); + deepEquals(fill(i32_1, 65537), i32_2.fill(65537)); + deepEquals(fill(i32_1, -1), i32_2.fill(-1)); + const f32_1 = new Float32Array(5); + const f32_2 = new Float32Array(5); + deepEquals(fill(f32_1, 1.2345), f32_2.fill(1.2345)); + const f64_1 = new Float64Array(5); + const f64_2 = new Float64Array(5); + deepEquals(fill(f64_1, 1.2345), f64_2.fill(1.2345)); + const u8Clamped_1 = new Uint8ClampedArray(5); + const u8Clamped_2 = new Uint8ClampedArray(5); + deepEquals(fill(u8Clamped_1, 2), u8Clamped_2.fill(2)); + deepEquals(fill(u8Clamped_1, 257), u8Clamped_2.fill(257)); + }); + it('should work with all typed array types - explicit looping', function(): void { + const u8_1 = new Uint8Array(5); + const u8_2 = new Uint8Array(5); + deepEquals(loopFill(u8_1, 2), u8_2.fill(2)); + deepEquals(loopFill(u8_1, -1), u8_2.fill(-1)); + const u16_1 = new Uint16Array(5); + const u16_2 = new Uint16Array(5); + deepEquals(loopFill(u16_1, 2), u16_2.fill(2)); + deepEquals(loopFill(u16_1, 65535), u16_2.fill(65535)); + deepEquals(loopFill(u16_1, -1), u16_2.fill(-1)); + const u32_1 = new Uint32Array(5); + const u32_2 = new Uint32Array(5); + deepEquals(loopFill(u32_1, 2), u32_2.fill(2)); + deepEquals(loopFill(u32_1, 65537), u32_2.fill(65537)); + deepEquals(loopFill(u32_1, -1), u32_2.fill(-1)); + const i8_1 = new Int8Array(5); + const i8_2 = new Int8Array(5); + deepEquals(loopFill(i8_1, 2), i8_2.fill(2)); + deepEquals(loopFill(i8_1, -1), i8_2.fill(-1)); + const i16_1 = new Int16Array(5); + const i16_2 = new Int16Array(5); + deepEquals(loopFill(i16_1, 2), i16_2.fill(2)); + deepEquals(loopFill(i16_1, 65535), i16_2.fill(65535)); + deepEquals(loopFill(i16_1, -1), i16_2.fill(-1)); + const i32_1 = new Int32Array(5); + const i32_2 = new Int32Array(5); + deepEquals(loopFill(i32_1, 2), i32_2.fill(2)); + deepEquals(loopFill(i32_1, 65537), i32_2.fill(65537)); + deepEquals(loopFill(i32_1, -1), i32_2.fill(-1)); + const f32_1 = new Float32Array(5); + const f32_2 = new Float32Array(5); + deepEquals(loopFill(f32_1, 1.2345), f32_2.fill(1.2345)); + const f64_1 = new Float64Array(5); + const f64_2 = new Float64Array(5); + deepEquals(loopFill(f64_1, 1.2345), f64_2.fill(1.2345)); + const u8Clamped_1 = new Uint8ClampedArray(5); + const u8Clamped_2 = new Uint8ClampedArray(5); + deepEquals(loopFill(u8Clamped_1, 2), u8Clamped_2.fill(2)); + deepEquals(loopFill(u8Clamped_1, 257), u8Clamped_2.fill(257)); + }); + it('start offset', function(): void { + for (let i = -2; i < 10; ++i) { + const u8_1 = new Uint8Array(5); + const u8_2 = new Uint8Array(5); + const u8_3 = new Uint8Array(5); + deepEquals(fill(u8_1, 2, i), u8_3.fill(2, i)); + deepEquals(fill(u8_1, -1, i), u8_3.fill(-1, i)); + deepEquals(loopFill(u8_2, 2, i), u8_3.fill(2, i)); + deepEquals(loopFill(u8_2, -1, i), u8_3.fill(-1, i)); + } + }); + it('end offset', function(): void { + for (let i = -2; i < 10; ++i) { + const u8_1 = new Uint8Array(5); + const u8_2 = new Uint8Array(5); + const u8_3 = new Uint8Array(5); + deepEquals(fill(u8_1, 2, 0, i), u8_3.fill(2, 0, i)); + deepEquals(fill(u8_1, -1, 0, i), u8_3.fill(-1, 0, i)); + deepEquals(loopFill(u8_2, 2, 0, i), u8_3.fill(2, 0, i)); + deepEquals(loopFill(u8_2, -1, 0, i), u8_3.fill(-1, 0, i)); + } + }); + it('start/end offset', function(): void { + for (let i = -2; i < 10; ++i) { + for (let j = -2; j < 10; ++j) { + const u8_1 = new Uint8Array(5); + const u8_2 = new Uint8Array(5); + const u8_3 = new Uint8Array(5); + deepEquals(fill(u8_1, 2, i, j), u8_3.fill(2, i, j)); + deepEquals(fill(u8_1, -1, i, j), u8_3.fill(-1, i, j)); + deepEquals(loopFill(u8_2, 2, i, j), u8_3.fill(2, i, j)); + deepEquals(loopFill(u8_2, -1, i, j), u8_3.fill(-1, i, j)); + } + } + }); + }); +}); diff --git a/src/core/TypedArrayUtils.ts b/src/core/TypedArrayUtils.ts new file mode 100644 index 00000000..56e9d7b0 --- /dev/null +++ b/src/core/TypedArrayUtils.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * polyfill for TypedArray.fill + * This is needed to support .fill in all safari versions and IE 11. + */ + +type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + +export function fill(array: TypedArray, value: number, start: number = 0, end?: number | undefined): TypedArray { + // all modern engines that support .fill + if (array.fill) { + return array.fill(value, start, end); + } + // safari and IE 11 + // since IE 11 does not support Array.prototype.fill either + // we cannot use the suggested polyfill from MDN + // instead we simply fall back to looping + if (start >= array.length) { + return array; + } + start = (array.length + start) % array.length; + if (end === undefined) { + end = array.length; + } + if (end >= array.length) { + end = array.length; + } else { + end = (array.length + end) % array.length; + } + for (let i = start; i < end; ++i) { + array[i] = value; + } + return array; +} From 8c33de395d48fa6ee2ff4c4c3e2de830a82e70a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 21 Nov 2018 00:15:04 +0100 Subject: [PATCH 55/64] fix linter errors --- src/core/TypedArrayUtils.test.ts | 211 ++++++++++++++++--------------- 1 file changed, 106 insertions(+), 105 deletions(-) diff --git a/src/core/TypedArrayUtils.test.ts b/src/core/TypedArrayUtils.test.ts index 2e45d371..ef86d314 100644 --- a/src/core/TypedArrayUtils.test.ts +++ b/src/core/TypedArrayUtils.test.ts @@ -9,6 +9,7 @@ type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; +// we explicitly test against the looping version in the test cases function loopFill(array: TypedArray, value: number, start: number = 0, end?: number | undefined): TypedArray { if (start >= array.length) { return array; @@ -28,128 +29,128 @@ function loopFill(array: TypedArray, value: number, start: number = 0, end?: num return array; } -function deepEquals(a: TypedArray, b: TypedArray) { - assert.equal(a.length, b.length); - for (let i = 0; i < a.length; ++i) { - assert.equal(a[i], b[i]); - } -} - - describe('polyfill conformance tests', function(): void { + + function deepEquals(a: TypedArray, b: TypedArray): void { + assert.equal(a.length, b.length); + for (let i = 0; i < a.length; ++i) { + assert.equal(a[i], b[i]); + } + } + describe('TypedArray.fill', function(): void { it('should work with all typed array types', function(): void { - const u8_1 = new Uint8Array(5); - const u8_2 = new Uint8Array(5); - deepEquals(fill(u8_1, 2), u8_2.fill(2)); - deepEquals(fill(u8_1, -1), u8_2.fill(-1)); - const u16_1 = new Uint16Array(5); - const u16_2 = new Uint16Array(5); - deepEquals(fill(u16_1, 2), u16_2.fill(2)); - deepEquals(fill(u16_1, 65535), u16_2.fill(65535)); - deepEquals(fill(u16_1, -1), u16_2.fill(-1)); - const u32_1 = new Uint32Array(5); - const u32_2 = new Uint32Array(5); - deepEquals(fill(u32_1, 2), u32_2.fill(2)); - deepEquals(fill(u32_1, 65537), u32_2.fill(65537)); - deepEquals(fill(u32_1, -1), u32_2.fill(-1)); - const i8_1 = new Int8Array(5); - const i8_2 = new Int8Array(5); - deepEquals(fill(i8_1, 2), i8_2.fill(2)); - deepEquals(fill(i8_1, -1), i8_2.fill(-1)); - const i16_1 = new Int16Array(5); - const i16_2 = new Int16Array(5); - deepEquals(fill(i16_1, 2), i16_2.fill(2)); - deepEquals(fill(i16_1, 65535), i16_2.fill(65535)); - deepEquals(fill(i16_1, -1), i16_2.fill(-1)); - const i32_1 = new Int32Array(5); - const i32_2 = new Int32Array(5); - deepEquals(fill(i32_1, 2), i32_2.fill(2)); - deepEquals(fill(i32_1, 65537), i32_2.fill(65537)); - deepEquals(fill(i32_1, -1), i32_2.fill(-1)); - const f32_1 = new Float32Array(5); - const f32_2 = new Float32Array(5); - deepEquals(fill(f32_1, 1.2345), f32_2.fill(1.2345)); - const f64_1 = new Float64Array(5); - const f64_2 = new Float64Array(5); - deepEquals(fill(f64_1, 1.2345), f64_2.fill(1.2345)); - const u8Clamped_1 = new Uint8ClampedArray(5); - const u8Clamped_2 = new Uint8ClampedArray(5); - deepEquals(fill(u8Clamped_1, 2), u8Clamped_2.fill(2)); - deepEquals(fill(u8Clamped_1, 257), u8Clamped_2.fill(257)); + const u81 = new Uint8Array(5); + const u82 = new Uint8Array(5); + deepEquals(fill(u81, 2), u82.fill(2)); + deepEquals(fill(u81, -1), u82.fill(-1)); + const u161 = new Uint16Array(5); + const u162 = new Uint16Array(5); + deepEquals(fill(u161, 2), u162.fill(2)); + deepEquals(fill(u161, 65535), u162.fill(65535)); + deepEquals(fill(u161, -1), u162.fill(-1)); + const u321 = new Uint32Array(5); + const u322 = new Uint32Array(5); + deepEquals(fill(u321, 2), u322.fill(2)); + deepEquals(fill(u321, 65537), u322.fill(65537)); + deepEquals(fill(u321, -1), u322.fill(-1)); + const i81 = new Int8Array(5); + const i82 = new Int8Array(5); + deepEquals(fill(i81, 2), i82.fill(2)); + deepEquals(fill(i81, -1), i82.fill(-1)); + const i161 = new Int16Array(5); + const i162 = new Int16Array(5); + deepEquals(fill(i161, 2), i162.fill(2)); + deepEquals(fill(i161, 65535), i162.fill(65535)); + deepEquals(fill(i161, -1), i162.fill(-1)); + const i321 = new Int32Array(5); + const i322 = new Int32Array(5); + deepEquals(fill(i321, 2), i322.fill(2)); + deepEquals(fill(i321, 65537), i322.fill(65537)); + deepEquals(fill(i321, -1), i322.fill(-1)); + const f321 = new Float32Array(5); + const f322 = new Float32Array(5); + deepEquals(fill(f321, 1.2345), f322.fill(1.2345)); + const f641 = new Float64Array(5); + const f642 = new Float64Array(5); + deepEquals(fill(f641, 1.2345), f642.fill(1.2345)); + const u8Clamped1 = new Uint8ClampedArray(5); + const u8Clamped2 = new Uint8ClampedArray(5); + deepEquals(fill(u8Clamped1, 2), u8Clamped2.fill(2)); + deepEquals(fill(u8Clamped1, 257), u8Clamped2.fill(257)); }); it('should work with all typed array types - explicit looping', function(): void { - const u8_1 = new Uint8Array(5); - const u8_2 = new Uint8Array(5); - deepEquals(loopFill(u8_1, 2), u8_2.fill(2)); - deepEquals(loopFill(u8_1, -1), u8_2.fill(-1)); - const u16_1 = new Uint16Array(5); - const u16_2 = new Uint16Array(5); - deepEquals(loopFill(u16_1, 2), u16_2.fill(2)); - deepEquals(loopFill(u16_1, 65535), u16_2.fill(65535)); - deepEquals(loopFill(u16_1, -1), u16_2.fill(-1)); - const u32_1 = new Uint32Array(5); - const u32_2 = new Uint32Array(5); - deepEquals(loopFill(u32_1, 2), u32_2.fill(2)); - deepEquals(loopFill(u32_1, 65537), u32_2.fill(65537)); - deepEquals(loopFill(u32_1, -1), u32_2.fill(-1)); - const i8_1 = new Int8Array(5); - const i8_2 = new Int8Array(5); - deepEquals(loopFill(i8_1, 2), i8_2.fill(2)); - deepEquals(loopFill(i8_1, -1), i8_2.fill(-1)); - const i16_1 = new Int16Array(5); - const i16_2 = new Int16Array(5); - deepEquals(loopFill(i16_1, 2), i16_2.fill(2)); - deepEquals(loopFill(i16_1, 65535), i16_2.fill(65535)); - deepEquals(loopFill(i16_1, -1), i16_2.fill(-1)); - const i32_1 = new Int32Array(5); - const i32_2 = new Int32Array(5); - deepEquals(loopFill(i32_1, 2), i32_2.fill(2)); - deepEquals(loopFill(i32_1, 65537), i32_2.fill(65537)); - deepEquals(loopFill(i32_1, -1), i32_2.fill(-1)); - const f32_1 = new Float32Array(5); - const f32_2 = new Float32Array(5); - deepEquals(loopFill(f32_1, 1.2345), f32_2.fill(1.2345)); - const f64_1 = new Float64Array(5); - const f64_2 = new Float64Array(5); - deepEquals(loopFill(f64_1, 1.2345), f64_2.fill(1.2345)); - const u8Clamped_1 = new Uint8ClampedArray(5); - const u8Clamped_2 = new Uint8ClampedArray(5); - deepEquals(loopFill(u8Clamped_1, 2), u8Clamped_2.fill(2)); - deepEquals(loopFill(u8Clamped_1, 257), u8Clamped_2.fill(257)); + const u81 = new Uint8Array(5); + const u82 = new Uint8Array(5); + deepEquals(loopFill(u81, 2), u82.fill(2)); + deepEquals(loopFill(u81, -1), u82.fill(-1)); + const u161 = new Uint16Array(5); + const u162 = new Uint16Array(5); + deepEquals(loopFill(u161, 2), u162.fill(2)); + deepEquals(loopFill(u161, 65535), u162.fill(65535)); + deepEquals(loopFill(u161, -1), u162.fill(-1)); + const u321 = new Uint32Array(5); + const u322 = new Uint32Array(5); + deepEquals(loopFill(u321, 2), u322.fill(2)); + deepEquals(loopFill(u321, 65537), u322.fill(65537)); + deepEquals(loopFill(u321, -1), u322.fill(-1)); + const i81 = new Int8Array(5); + const i82 = new Int8Array(5); + deepEquals(loopFill(i81, 2), i82.fill(2)); + deepEquals(loopFill(i81, -1), i82.fill(-1)); + const i161 = new Int16Array(5); + const i162 = new Int16Array(5); + deepEquals(loopFill(i161, 2), i162.fill(2)); + deepEquals(loopFill(i161, 65535), i162.fill(65535)); + deepEquals(loopFill(i161, -1), i162.fill(-1)); + const i321 = new Int32Array(5); + const i322 = new Int32Array(5); + deepEquals(loopFill(i321, 2), i322.fill(2)); + deepEquals(loopFill(i321, 65537), i322.fill(65537)); + deepEquals(loopFill(i321, -1), i322.fill(-1)); + const f321 = new Float32Array(5); + const f322 = new Float32Array(5); + deepEquals(loopFill(f321, 1.2345), f322.fill(1.2345)); + const f641 = new Float64Array(5); + const f642 = new Float64Array(5); + deepEquals(loopFill(f641, 1.2345), f642.fill(1.2345)); + const u8Clamped1 = new Uint8ClampedArray(5); + const u8Clamped2 = new Uint8ClampedArray(5); + deepEquals(loopFill(u8Clamped1, 2), u8Clamped2.fill(2)); + deepEquals(loopFill(u8Clamped1, 257), u8Clamped2.fill(257)); }); it('start offset', function(): void { for (let i = -2; i < 10; ++i) { - const u8_1 = new Uint8Array(5); - const u8_2 = new Uint8Array(5); - const u8_3 = new Uint8Array(5); - deepEquals(fill(u8_1, 2, i), u8_3.fill(2, i)); - deepEquals(fill(u8_1, -1, i), u8_3.fill(-1, i)); - deepEquals(loopFill(u8_2, 2, i), u8_3.fill(2, i)); - deepEquals(loopFill(u8_2, -1, i), u8_3.fill(-1, i)); + const u81 = new Uint8Array(5); + const u82 = new Uint8Array(5); + const u83 = new Uint8Array(5); + deepEquals(fill(u81, 2, i), u83.fill(2, i)); + deepEquals(fill(u81, -1, i), u83.fill(-1, i)); + deepEquals(loopFill(u82, 2, i), u83.fill(2, i)); + deepEquals(loopFill(u82, -1, i), u83.fill(-1, i)); } }); it('end offset', function(): void { for (let i = -2; i < 10; ++i) { - const u8_1 = new Uint8Array(5); - const u8_2 = new Uint8Array(5); - const u8_3 = new Uint8Array(5); - deepEquals(fill(u8_1, 2, 0, i), u8_3.fill(2, 0, i)); - deepEquals(fill(u8_1, -1, 0, i), u8_3.fill(-1, 0, i)); - deepEquals(loopFill(u8_2, 2, 0, i), u8_3.fill(2, 0, i)); - deepEquals(loopFill(u8_2, -1, 0, i), u8_3.fill(-1, 0, i)); + const u81 = new Uint8Array(5); + const u82 = new Uint8Array(5); + const u83 = new Uint8Array(5); + deepEquals(fill(u81, 2, 0, i), u83.fill(2, 0, i)); + deepEquals(fill(u81, -1, 0, i), u83.fill(-1, 0, i)); + deepEquals(loopFill(u82, 2, 0, i), u83.fill(2, 0, i)); + deepEquals(loopFill(u82, -1, 0, i), u83.fill(-1, 0, i)); } }); it('start/end offset', function(): void { for (let i = -2; i < 10; ++i) { for (let j = -2; j < 10; ++j) { - const u8_1 = new Uint8Array(5); - const u8_2 = new Uint8Array(5); - const u8_3 = new Uint8Array(5); - deepEquals(fill(u8_1, 2, i, j), u8_3.fill(2, i, j)); - deepEquals(fill(u8_1, -1, i, j), u8_3.fill(-1, i, j)); - deepEquals(loopFill(u8_2, 2, i, j), u8_3.fill(2, i, j)); - deepEquals(loopFill(u8_2, -1, i, j), u8_3.fill(-1, i, j)); + const u81 = new Uint8Array(5); + const u82 = new Uint8Array(5); + const u83 = new Uint8Array(5); + deepEquals(fill(u81, 2, i, j), u83.fill(2, i, j)); + deepEquals(fill(u81, -1, i, j), u83.fill(-1, i, j)); + deepEquals(loopFill(u82, 2, i, j), u83.fill(2, i, j)); + deepEquals(loopFill(u82, -1, i, j), u83.fill(-1, i, j)); } } }); From 98a3892e52408afcbc599dc6c7943b9558e1a060 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 22 Nov 2018 09:44:13 -0800 Subject: [PATCH 56/64] Fix typo --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c389f5b..e7924027 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ opening an issue, read these pointers. ## Contributing code -You can find issues to work on my looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: +You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: - Fork [xterm.js](https://github.com/sourcelair/xterm.js/) ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) From 53fe096455968f8d8158939ea50a929cc3b6e5a2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 22 Nov 2018 10:52:55 -0800 Subject: [PATCH 57/64] Type fn strongly --- src/addons/fullscreen/fullscreen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index ef9b8ffd..4d05e904 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -11,7 +11,7 @@ import { Terminal } from 'xterm'; * @param fullscreen Toggle fullscreen on (true) or off (false) */ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { - let fn: Function; + let fn: (...tokens: string[]) => void; if (typeof fullscreen === 'undefined') { fn = (term.element.classList.contains('fullscreen')) ? From 692cdfa390943c0133a8fbd1a27e6b1a9ba721a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 22 Nov 2018 11:32:08 -0800 Subject: [PATCH 58/64] DOM renderer: Don't output empty cells at end Fixes #1609 --- src/renderer/dom/DomRendererRowFactory.ts | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 4bb59902..bcb0cc48 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE } from '../../Buffer'; import { FLAGS } from '../Types'; import { IBufferLine } from '../../Types'; @@ -23,17 +23,28 @@ export class DomRendererRowFactory { public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); let colCount = 0; + let nonNullCellFound = false; - for (let x = 0; x < lineData.length; x++) { + for (let x = lineData.length - 1; x >= 0; x--) { // Don't allow any buffer to the right to be displayed if (colCount >= cols) { continue; } const charData = lineData.get(x); - const char: string = charData[CHAR_DATA_CHAR_INDEX]; - const attr: number = charData[CHAR_DATA_ATTR_INDEX]; - const width: number = charData[CHAR_DATA_WIDTH_INDEX]; + + if (!nonNullCellFound) { + const code = charData[CHAR_DATA_CODE_INDEX]; + if (code === NULL_CELL_CODE && !(isCursorRow && x === cursorX)) { + continue; + } else { + nonNullCellFound = true; + } + } + + const char = charData[CHAR_DATA_CHAR_INDEX]; + const attr = charData[CHAR_DATA_ATTR_INDEX]; + const width = charData[CHAR_DATA_WIDTH_INDEX]; // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -97,7 +108,11 @@ export class DomRendererRowFactory { if (bg !== 256) { charElement.classList.add(`xterm-bg-${bg}`); } - fragment.appendChild(charElement); + if (fragment.childNodes.length === 0) { + fragment.appendChild(charElement); + } else { + fragment.insertBefore(charElement, fragment.firstChild); + } colCount += width; } return fragment; From dc077a181e3b4f34c49f22f9579b1fd9dca653ff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 22 Nov 2018 12:03:08 -0800 Subject: [PATCH 59/64] Fix tests and the colCount feature --- .../dom/DomRendererRowFactory.test.ts | 32 ++++++----------- src/renderer/dom/DomRendererRowFactory.ts | 35 +++++++++---------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 2c46d8cc..03f41784 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -23,11 +23,10 @@ describe('DomRendererRowFactory', () => { }); describe('createRow', () => { - it('should create an element for every character in the row', () => { + it('should not create anything for an empty row', () => { const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - ' ' + - ' ' + '' ); }); @@ -45,8 +44,7 @@ describe('DomRendererRowFactory', () => { for (const style of ['block', 'bar', 'underline']) { const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - ` ` + - ' ' + ` ` ); } }); @@ -65,8 +63,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); @@ -74,8 +71,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); @@ -85,8 +81,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - `a` + - ' ' + `a` ); } }); @@ -97,8 +92,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - `a` + - ' ' + `a` ); } }); @@ -107,8 +101,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); @@ -116,8 +109,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); @@ -125,8 +117,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); @@ -135,8 +126,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - `a` + - ' ' + `a` ); } }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index bcb0cc48..5f6b49fc 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -22,26 +22,29 @@ export class DomRendererRowFactory { public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); - let colCount = 0; - let nonNullCellFound = false; - for (let x = lineData.length - 1; x >= 0; x--) { + // Find the line length first, this prevents the need to output a bunch of + // empty cells at the end. This cannot easily be integrated into the main + // loop below because of the colCount feature (which can be removed after we + // properly support reflow and disallow data to go beyond the right-side of + // the viewport). + let lineLength = 0; + for (let x = 0; x < lineData.length; x++) { + const charData = lineData.get(x); + const code = charData[CHAR_DATA_CODE_INDEX]; + if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + lineLength = x + 1; + } + } + + let colCount = 0; + for (let x = 0; x < lineLength; x++) { // Don't allow any buffer to the right to be displayed if (colCount >= cols) { continue; } const charData = lineData.get(x); - - if (!nonNullCellFound) { - const code = charData[CHAR_DATA_CODE_INDEX]; - if (code === NULL_CELL_CODE && !(isCursorRow && x === cursorX)) { - continue; - } else { - nonNullCellFound = true; - } - } - const char = charData[CHAR_DATA_CHAR_INDEX]; const attr = charData[CHAR_DATA_ATTR_INDEX]; const width = charData[CHAR_DATA_WIDTH_INDEX]; @@ -108,11 +111,7 @@ export class DomRendererRowFactory { if (bg !== 256) { charElement.classList.add(`xterm-bg-${bg}`); } - if (fragment.childNodes.length === 0) { - fragment.appendChild(charElement); - } else { - fragment.insertBefore(charElement, fragment.firstChild); - } + fragment.appendChild(charElement); colCount += width; } return fragment; From 66272eb3105ff23b84e5a79b622a72e49352419c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Nov 2018 05:39:50 -0800 Subject: [PATCH 60/64] Get the line length by going backwards instead --- src/renderer/dom/DomRendererRowFactory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 5f6b49fc..490360bf 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -29,11 +29,12 @@ export class DomRendererRowFactory { // properly support reflow and disallow data to go beyond the right-side of // the viewport). let lineLength = 0; - for (let x = 0; x < lineData.length; x++) { + for (let x = lineData.length - 1; x >= 0; x--) { const charData = lineData.get(x); const code = charData[CHAR_DATA_CODE_INDEX]; if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; + break; } } From dc0ae4a8156bd0cfff6e1884065757c4c9b9098f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Nov 2018 06:24:27 -0800 Subject: [PATCH 61/64] Fix indentation --- src/CharWidth.ts | 248 +++++++++++++++++++++++------------------------ 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/src/CharWidth.ts b/src/CharWidth.ts index 341b347a..5a484da8 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -6,136 +6,136 @@ import { fill } from './core/TypedArrayUtils'; */ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { - // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c - // combining characters - const COMBINING_BMP = [ - [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], - [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], - [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], - [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], - [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], - [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], - [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], - [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], - [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], - [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], - [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], - [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], - [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], - [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], - [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], - [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], - [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], - [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], - [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], - [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], - [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], - [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], - [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], - [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], - [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], - [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], - [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], - [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], - [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], - [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], - [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], - [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], - [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], - [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], - [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], - [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], - [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], - [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], - [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], - [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], - [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], - [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], - [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] - ]; - const COMBINING_HIGH = [ - [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], - [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], - [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], - [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], - [0xE0100, 0xE01EF] - ]; - // binary search - function bisearch(ucs: number, data: number[][]): boolean { - let min = 0; - let max = data.length - 1; - let mid; - if (ucs < data[0][0] || ucs > data[max][1]) { - return false; - } - while (max >= min) { - mid = (min + max) >> 1; - if (ucs > data[mid][1]) { - min = mid + 1; - } else if (ucs < data[mid][0]) { - max = mid - 1; - } else { - return true; - } - } + // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c + // combining characters + const COMBINING_BMP = [ + [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], + [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], + [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], + [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], + [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], + [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], + [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], + [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], + [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], + [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], + [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], + [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], + [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], + [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], + [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], + [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], + [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], + [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], + [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], + [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], + [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], + [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], + [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], + [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], + [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], + [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], + [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], + [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], + [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], + [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], + [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], + [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], + [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], + [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], + [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], + [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], + [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], + [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], + [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], + [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], + [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], + [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] + ]; + const COMBINING_HIGH = [ + [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], + [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], + [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], + [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], + [0xE0100, 0xE01EF] + ]; + // binary search + function bisearch(ucs: number, data: number[][]): boolean { + let min = 0; + let max = data.length - 1; + let mid; + if (ucs < data[0][0] || ucs > data[max][1]) { return false; } - function wcwidthHigh(ucs: number): 0 | 1 | 2 { - if (bisearch(ucs, COMBINING_HIGH)) { - return 0; - } - if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { - return 2; + while (max >= min) { + mid = (min + max) >> 1; + if (ucs > data[mid][1]) { + min = mid + 1; + } else if (ucs < data[mid][0]) { + max = mid - 1; + } else { + return true; } + } + return false; + } + function wcwidthHigh(ucs: number): 0 | 1 | 2 { + if (bisearch(ucs, COMBINING_HIGH)) { + return 0; + } + if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { + return 2; + } + return 1; + } + const control = opts.control | 0; + + // create lookup table for BMP plane + const table = new Uint8Array(65536); + fill(table, 1); + table[0] = opts.nul; + // control chars + fill(table, opts.control, 1, 32); + fill(table, opts.control, 0x7f, 0xa0); + + // apply wide char rules first + // wide chars + fill(table, 2, 0x1100, 0x1160); + table[0x2329] = 2; + table[0x232a] = 2; + fill(table, 2, 0x2e80, 0xa4d0); + table[0x303f] = 1; // wrongly in last line + + fill(table, 2, 0xac00, 0xd7a4); + fill(table, 2, 0xf900, 0xfb00); + fill(table, 2, 0xfe10, 0xfe1a); + fill(table, 2, 0xfe30, 0xfe70); + fill(table, 2, 0xff00, 0xff61); + fill(table, 2, 0xffe0, 0xffe7); + + // apply combining last to ensure we overwrite + // wrongly wide set chars: + // the original algo evals combining first and falls + // through to wide check so we simply do here the opposite + // combining 0 + for (let r = 0; r < COMBINING_BMP.length; ++r) { + fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1); + } + + return function (num: number): number { + if (num < 32) { + return control | 0; + } + if (num < 127) { return 1; } - const control = opts.control | 0; - - // create lookup table for BMP plane - const table = new Uint8Array(65536); - fill(table, 1); - table[0] = opts.nul; - // control chars - fill(table, opts.control, 1, 32); - fill(table, opts.control, 0x7f, 0xa0); - - // apply wide char rules first - // wide chars - fill(table, 2, 0x1100, 0x1160); - table[0x2329] = 2; - table[0x232a] = 2; - fill(table, 2, 0x2e80, 0xa4d0); - table[0x303f] = 1; // wrongly in last line - - fill(table, 2, 0xac00, 0xd7a4); - fill(table, 2, 0xf900, 0xfb00); - fill(table, 2, 0xfe10, 0xfe1a); - fill(table, 2, 0xfe30, 0xfe70); - fill(table, 2, 0xff00, 0xff61); - fill(table, 2, 0xffe0, 0xffe7); - - // apply combining last to ensure we overwrite - // wrongly wide set chars: - // the original algo evals combining first and falls - // through to wide check so we simply do here the opposite - // combining 0 - for (let r = 0; r < COMBINING_BMP.length; ++r) { - fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1); + if (num < 65536) { + return table[num]; } - - return function (num: number): number { - if (num < 32) { - return control | 0; - } - if (num < 127) { - return 1; - } - if (num < 65536) { - return table[num]; - } - // do a full search for high codepoints - return wcwidthHigh(num); - }; + // do a full search for high codepoints + return wcwidthHigh(num); + }; })({nul: 0, control: 0}); // configurable options /** From 65c5df92d913936a9902acc665229293a41acd27 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Nov 2018 06:26:06 -0800 Subject: [PATCH 62/64] Move import below copyright --- src/CharWidth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CharWidth.ts b/src/CharWidth.ts index 5a484da8..fd6ac55f 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -1,10 +1,10 @@ -import { fill } from './core/TypedArrayUtils'; - /** * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ +import { fill } from './core/TypedArrayUtils'; + export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c // combining characters From d9513ce50df9ea1faa8e4ef7da076824df3be171 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Nov 2018 06:48:37 -0800 Subject: [PATCH 63/64] Don't bother checking beyond cols --- src/renderer/dom/DomRendererRowFactory.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 490360bf..07303e24 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -29,7 +29,7 @@ export class DomRendererRowFactory { // properly support reflow and disallow data to go beyond the right-side of // the viewport). let lineLength = 0; - for (let x = lineData.length - 1; x >= 0; x--) { + for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { const charData = lineData.get(x); const code = charData[CHAR_DATA_CODE_INDEX]; if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { @@ -38,13 +38,7 @@ export class DomRendererRowFactory { } } - let colCount = 0; for (let x = 0; x < lineLength; x++) { - // Don't allow any buffer to the right to be displayed - if (colCount >= cols) { - continue; - } - const charData = lineData.get(x); const char = charData[CHAR_DATA_CHAR_INDEX]; const attr = charData[CHAR_DATA_ATTR_INDEX]; @@ -113,7 +107,6 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); } fragment.appendChild(charElement); - colCount += width; } return fragment; } From 478dfee6e2988fd81a61b4e85b14125b3e087e50 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Nov 2018 06:51:39 -0800 Subject: [PATCH 64/64] Ensure wide chars don't overflow onto following row --- src/renderer/dom/DomRenderer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index fadd9b72..73c63b46 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -111,6 +111,8 @@ export class DomRenderer extends EventEmitter implements IRenderer { element.style.width = `${this.dimensions.canvasWidth}px`; element.style.height = `${this.dimensions.actualCellHeight}px`; element.style.lineHeight = `${this.dimensions.actualCellHeight}px`; + // Make sure rows don't overflow onto following row + element.style.overflow = 'hidden'; }); if (!this._dimensionsStyleElement) { @@ -330,7 +332,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const row = y + terminal.buffer.ydisp; const lineData = terminal.buffer.lines.get(row); const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, terminal.charMeasure.width, terminal.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols)); } this._terminal.emit('refresh', {start, end});