From f3a37a9522df1ada8547852de5e2889239348d08 Mon Sep 17 00:00:00 2001 From: Jon Austin Date: Sat, 24 Feb 2018 10:14:51 -0800 Subject: [PATCH 01/18] Add Hyper to list of apps https://github.com/zeit/hyper/blob/82c7afbeecf5a0c455075868cd23585fce95cf68/package.json#L197 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cdaf0d71..1659bae4 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**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 - [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users +- [**Hyper**](https://hyper.is): A terminal built on web technologies Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. From 22c4197a1d42ae76308f1d084370b2f975dc5981 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 2 Apr 2018 08:16:39 -0700 Subject: [PATCH 02/18] Lint addons --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 594c31a7..8d17b5cd 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "start": "node demo/app", "prestart-zmodem": "npm run build", "start-zmodem": "node build/addons/zmodem/demo/app", - "lint": "tslint src/*.ts src/**/*.ts", + "lint": "tslint src/*.ts src/**/*.ts src/addons/**/*.ts", "test": "gulp test", "build:docs": "jsdoc -c jsdoc.json", "tsc": "tsc", From 768539a57c94891c3bc89a0a1823d0134fea3a5c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 2 Apr 2018 08:17:44 -0700 Subject: [PATCH 03/18] Fix lint errors in attach --- src/addons/attach/attach.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index b3b2e993..4a9668de 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -43,7 +43,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean addonTerminal.__getMessage = function(ev: MessageEvent): void { let str; - if (typeof ev.data == 'object') { + if (typeof ev.data === 'object') { if (!myTextDecoder) { myTextDecoder = new TextDecoder(); } @@ -53,14 +53,14 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean } else { let fileReader = new FileReader(); - fileReader.addEventListener('load', function() { + fileReader.addEventListener('load', () => { str = myTextDecoder.decode(this.result); displayData(str); }); fileReader.readAsArrayBuffer(ev.data); } - } else if (typeof ev.data == 'string') { - displayData(ev.data) + } else if (typeof ev.data === 'string') { + displayData(ev.data); } else { throw Error(`Cannot handle "${typeof ev.data}" websocket message.`); } @@ -73,7 +73,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean * @param str String decoded by FileReader. * @param data The data of the EventMessage. */ - function displayData(str?: string, data?: string) { + function displayData(str?: string, data?: string): void { if (buffered) { addonTerminal.__pushToBuffer(str || data); } else { From 87f21f14a1f59286f272b22bc396d540d01ba636 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Apr 2018 11:20:25 -0700 Subject: [PATCH 04/18] Remove unused options Fixes #897 --- src/Terminal.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index e441a5b4..cfbf8025 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -120,8 +120,6 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac - // programFeatures: false, - // focusKeys: false, }; export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal { From e5ee76ded4f61b8f250ed80f46af44466744b581 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Apr 2018 11:51:50 -0700 Subject: [PATCH 05/18] Remove terminal references on dispose Part of #1341 Fixes #1370 --- src/EventEmitter.ts | 4 +-- src/Terminal.ts | 44 +++++++++++++++++++++------------ src/renderer/atlas/CharAtlas.ts | 21 ++++++++++++++++ src/utils/TestUtils.test.ts | 3 +++ typings/xterm.d.ts | 10 +++++++- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index cece1b9d..0698386e 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -6,7 +6,7 @@ import { XtermListener } from './Types'; import { IEventEmitter, IDisposable } from 'xterm'; -export class EventEmitter implements IEventEmitter { +export class EventEmitter implements IEventEmitter, IDisposable { private _events: {[type: string]: XtermListener[]}; constructor() { @@ -75,7 +75,7 @@ export class EventEmitter implements IEventEmitter { return this._events[type] || []; } - protected destroy(): void { + public dispose(): void { this._events = {}; } } diff --git a/src/Terminal.ts b/src/Terminal.ts index e441a5b4..090816b1 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -38,6 +38,7 @@ import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './shared/utils/Browser'; +import * as Dom from './utils/Dom'; import * as Strings from './Strings'; import { MouseHelper } from './utils/MouseHelper'; import { clone } from './utils/Clone'; @@ -46,7 +47,8 @@ import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './input/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; -import { ITheme, ILocalizableStrings, IMarker } from 'xterm'; +import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm'; +import { removeTerminalFromCache } from './renderer/atlas/CharAtlas'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -124,11 +126,13 @@ const DEFAULT_OPTIONS: ITerminalOptions = { // focusKeys: false, }; -export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal { +export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; public screenElement: HTMLElement; + private _disposables: IDisposable[]; + /** * The HTMLElement that the terminal is created in, set by Terminal.open. */ @@ -250,7 +254,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this._setup(); } + public dispose(): void { + super.dispose(); + this._disposables.forEach(d => d.dispose()); + this._disposables.length = 0; + removeTerminalFromCache(this); + this.handler = () => {}; + this.write = () => {}; + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + } + + /** + * @deprecated Use dispose instead. + */ + public destroy(): void { + this.dispose(); + } + private _setup(): void { + this._disposables = []; + Object.keys(DEFAULT_OPTIONS).forEach((key) => { if (this.options[key] == null) { this.options[key] = DEFAULT_OPTIONS[key]; @@ -686,7 +711,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. - window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); + this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows)); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); @@ -1079,19 +1104,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }); } - /** - * Destroys the terminal. - */ - public destroy(): void { - super.destroy(); - this.handler = () => {}; - this.write = () => {}; - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - // this.emit('close'); - } - /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. diff --git a/src/renderer/atlas/CharAtlas.ts b/src/renderer/atlas/CharAtlas.ts index 2e417c09..52860426 100644 --- a/src/renderer/atlas/CharAtlas.ts +++ b/src/renderer/atlas/CharAtlas.ts @@ -26,6 +26,8 @@ let charAtlasCache: ICharAtlasCacheEntry[] = []; export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise { const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); + // TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed) + // Check to see if the terminal already owns this config for (let i = 0; i < charAtlasCache.length; i++) { const entry = charAtlasCache[i]; @@ -70,3 +72,22 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC charAtlasCache.push(newEntry); return newEntry.bitmap; } + +/** + * Removes a terminal reference from the cache, allowing its memory to be freed. + * @param terminal The terminal to remove. + */ +export function removeTerminalFromCache(terminal: ITerminal): void { + for (let i = 0; i < charAtlasCache.length; i++) { + const index = charAtlasCache[i].ownedBy.indexOf(terminal); + if (index !== -1) { + if (charAtlasCache[i].ownedBy.length === 1) { + // Remove the cache entry if it's the only terminal + charAtlasCache.splice(i, 1); + } else { + // Remove the reference from the cache entry + charAtlasCache[i].ownedBy.splice(index, 1); + } + } + } +} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 5f3a8482..4436c526 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -63,6 +63,9 @@ export class MockTerminal implements ITerminal { selectAll(): void { throw new Error('Method not implemented.'); } + dispose(): void { + throw new Error('Method not implemented.'); + } destroy(): void { throw new Error('Method not implemented.'); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 5c9e95e5..65d533de 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -251,7 +251,7 @@ declare module 'xterm' { /** * The class that represents an xterm.js terminal. */ - export class Terminal implements IEventEmitter { + export class Terminal implements IEventEmitter, IDisposable { /** * The element containing the terminal. */ @@ -451,8 +451,16 @@ declare module 'xterm' { */ selectLines(start: number, end: number): void; + /* + * Disposes of the terminal, detaching it from the DOM and removing any + * active listeners. + */ + dispose(): void; + /** * Destroys the terminal and detaches it from the DOM. + * + * @deprecated Use dispose() instead. */ destroy(): void; From e3dc36c123c187d239a8bf41431bf349efcf9ef0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 16 Apr 2018 06:28:45 -0700 Subject: [PATCH 06/18] Add xterm-webfont addon to readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 26f98382..e1c4bd29 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,12 @@ var xterm = new Terminal(); // Instantiate the terminal xterm.fit(); // Use the `fit` method, provided by the `fit` addon ``` +#### Third party addons + +There are also the following third party addons available: + +- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont) + ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: From 7d108999ff687fed5d50bee4271d66780208dc5e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 16 Apr 2018 06:42:16 -0700 Subject: [PATCH 07/18] Fix lint --- src/addons/zmodem/zmodem.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts index 297ebe34..4f2f4a91 100644 --- a/src/addons/zmodem/zmodem.ts +++ b/src/addons/zmodem/zmodem.ts @@ -43,8 +43,7 @@ export interface IZmodemOptions { } function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { - var term = this; - + const term = this; const senderFunc = (octets: ArrayLike) => ws.send(new Uint8Array(octets)); let zsentry; From b853aa492914bbaef91a73eeae0c904b621bfd9e Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 14 Mar 2018 10:05:29 -0700 Subject: [PATCH 08/18] Separate foreground & background rendering passes I'm primarily interested in doing this because it'll allow me to optimize the background rendering in later diffs, but this may also make it possible to fix some minor rendering issues. Right now, if we draw a single-width character that overflows its cell's bounds, and the character to the right of it has a background, that background may cover up the first character's foreground. By drawing the background in a separate pass, we can avoid those cases. There's still some issues with how dirty regions are computed that makes rendering stuff like that flaky, but this at least gets us closer to "correct" rendering. Other terminal emulators (e.g. alacritty) render the foreground and background in separate passes: https://github.com/jwilm/alacritty/blob/1b7ffea/src/renderer/mod.rs#L766 --- src/renderer/TextRenderLayer.ts | 155 +++++++++++++++----------------- 1 file changed, 72 insertions(+), 83 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 2487a294..8b061511 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -48,21 +48,24 @@ export class TextRenderLayer extends BaseRenderLayer { this.clearAll(); } - public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { - // Resize has not been called yet - if (this._state.cache.length === 0) { - return; - } - + private _forEachCell( + terminal: ITerminal, + startRow: number, + endRow: number, + callback: ( + code: number, + char: string, + width: number, + x: number, + y: number, + fg: number, + bg: number, + flags: number + ) => void + ): void { for (let y = startRow; y <= endRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); - - this.clearCells(0, y, terminal.cols, 1); - // for (let x = 0; x < terminal.cols; x++) { - // this._state.cache[x][y] = null; - // } - for (let x = 0; x < terminal.cols; x++) { const charData = line[x]; const code: number = charData[CHAR_DATA_CODE_INDEX]; @@ -73,51 +76,12 @@ export class TextRenderLayer extends BaseRenderLayer { // The character to the left is a wide character, drawing is owned by // the char at x-1 if (width === 0) { - // this._state.cache[x][y] = null; - continue; - } - - // If the character is a space and the character to the left is an - // overlapping character, skip the character and allow the overlapping - // char to take full control over this character's cell. - if (code === 32 /*' '*/) { - if (x > 0) { - const previousChar: CharData = line[x - 1]; - if (this._isOverlapping(previousChar)) { - continue; - } - } - } - - // Skip rendering if the character is identical - // const state = this._state.cache[x][y]; - // if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) { - // // Skip render, contents are identical - // this._state.cache[x][y] = charData; - // continue; - // } - - // Clear the old character was not a space with the default background - // const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE); - // if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) { - // this._clearChar(x, y); - // } - // this._state.cache[x][y] = charData; - - const flags = attr >> 18; - let bg = attr & 0x1ff; - - // Skip rendering if the character is invisible - const isDefaultBackground = bg >= 256; - const isInvisible = flags & FLAGS.INVISIBLE; - const isInverted = flags & FLAGS.INVERSE; - if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) { continue; } // If the character is an overlapping char and the character to the right is a // space, take ownership of the cell to the right. - if (width !== 0 && this._isOverlapping(charData)) { + if (this._isOverlapping(charData)) { // If the character is overlapping, we want to force a re-render on every // frame. This is specifically to work around the case where two // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a @@ -135,10 +99,12 @@ export class TextRenderLayer extends BaseRenderLayer { } } + const flags = attr >> 18; + let bg = attr & 0x1ff; let fg = (attr >> 9) & 0x1ff; // If inverse flag is on, the foreground should become the background. - if (isInverted) { + if (flags & FLAGS.INVERSE) { const temp = bg; bg = fg; fg = temp; @@ -150,47 +116,70 @@ export class TextRenderLayer extends BaseRenderLayer { } } - // Clear the cell next to this character if it's wide - if (width === 2) { - // this.clearCells(x + 1, y, 1, 1); - } - - // Draw background - if (bg < 256) { - this._ctx.save(); - this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css); - this.fillCells(x, y, width, 1); - this._ctx.restore(); - } - - this._ctx.save(); if (flags & FLAGS.BOLD) { - this._ctx.font = this._getFont(terminal, true); // Convert the FG color to the bold variant if (fg < 8) { fg += 8; } } - if (flags & FLAGS.UNDERLINE) { - if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; - } else if (fg < 256) { - // 256 color support - this._ctx.fillStyle = this._colors.ansi[fg].css; - } else { - this._ctx.fillStyle = this._colors.foreground.css; - } - this.fillBottomLineAtCells(x, y); - } - - this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM)); - - this._ctx.restore(); + callback(code, char, width, x, y, fg, bg, flags); } } } + private _drawBackground(terminal: ITerminal, startRow: number, endRow: number): void { + this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + // libvte and xterm both draw the background (but not foreground) of invisible characters, + // so we should too. + const isDefaultBackground = bg >= 256; + if (!isDefaultBackground) { + this._ctx.save(); + this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css); + this.fillCells(x, y, width, 1); + this._ctx.restore(); + } + }); + } + + private _drawForeground(terminal: ITerminal, startRow: number, endRow: number): void { + this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + if (flags & FLAGS.INVISIBLE) { + return; + } + if (flags & FLAGS.UNDERLINE) { + this._ctx.save(); + if (fg === INVERTED_DEFAULT_COLOR) { + this._ctx.fillStyle = this._colors.background.css; + } else if (fg < 256) { + // 256 color support + this._ctx.fillStyle = this._colors.ansi[fg].css; + } else { + this._ctx.fillStyle = this._colors.foreground.css; + } + this.fillBottomLineAtCells(x, y); + this._ctx.restore(); + } + this.drawChar( + terminal, char, code, + width, x, y, + fg, bg, + !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM) + ); + }); + } + + public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { + // Resize has not been called yet + if (this._state.cache.length === 0) { + return; + } + + this.clearCells(0, startRow, terminal.cols, endRow - startRow + 1); // endRow is inclusive + this._drawBackground(terminal, startRow, endRow); + this._drawForeground(terminal, startRow, endRow); + } + public onOptionsChanged(terminal: ITerminal): void { this.setTransparency(terminal, terminal.options.allowTransparency); } From 58c50f08db6fadbf14f120b4f6f72714d5536b55 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Apr 2018 08:10:05 -0700 Subject: [PATCH 09/18] Break when the terminal reference is removed --- src/renderer/atlas/CharAtlas.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/atlas/CharAtlas.ts b/src/renderer/atlas/CharAtlas.ts index 52860426..f516919f 100644 --- a/src/renderer/atlas/CharAtlas.ts +++ b/src/renderer/atlas/CharAtlas.ts @@ -88,6 +88,7 @@ export function removeTerminalFromCache(terminal: ITerminal): void { // Remove the reference from the cache entry charAtlasCache[i].ownedBy.splice(index, 1); } + break; } } } From 0fe59df2e91e766f5bcec2e7e2479cb32a32bd3b Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 18 Apr 2018 20:22:55 -0700 Subject: [PATCH 10/18] Rename startRow/endRow in TextRenderLayer Renaming these to firstRow/lastRow makes the inclusivity of the range clearer. Addresses this comment: https://github.com/xtermjs/xterm.js/pull/1393#discussion_r182471582 --- src/renderer/TextRenderLayer.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 8b061511..58d0d790 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -50,8 +50,8 @@ export class TextRenderLayer extends BaseRenderLayer { private _forEachCell( terminal: ITerminal, - startRow: number, - endRow: number, + firstRow: number, + lastRow: number, callback: ( code: number, char: string, @@ -63,7 +63,7 @@ export class TextRenderLayer extends BaseRenderLayer { flags: number ) => void ): void { - for (let y = startRow; y <= endRow; y++) { + for (let y = firstRow; y <= lastRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); for (let x = 0; x < terminal.cols; x++) { @@ -128,8 +128,8 @@ export class TextRenderLayer extends BaseRenderLayer { } } - private _drawBackground(terminal: ITerminal, startRow: number, endRow: number): void { - this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void { + this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => { // libvte and xterm both draw the background (but not foreground) of invisible characters, // so we should too. const isDefaultBackground = bg >= 256; @@ -142,8 +142,8 @@ export class TextRenderLayer extends BaseRenderLayer { }); } - private _drawForeground(terminal: ITerminal, startRow: number, endRow: number): void { - this._forEachCell(terminal, startRow, endRow, (code, char, width, x, y, fg, bg, flags) => { + private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void { + this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => { if (flags & FLAGS.INVISIBLE) { return; } @@ -169,15 +169,15 @@ export class TextRenderLayer extends BaseRenderLayer { }); } - public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { + public onGridChanged(terminal: ITerminal, firstRow: number, lastRow: number): void { // Resize has not been called yet if (this._state.cache.length === 0) { return; } - this.clearCells(0, startRow, terminal.cols, endRow - startRow + 1); // endRow is inclusive - this._drawBackground(terminal, startRow, endRow); - this._drawForeground(terminal, startRow, endRow); + this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1); + this._drawBackground(terminal, firstRow, lastRow); + this._drawForeground(terminal, firstRow, lastRow); } public onOptionsChanged(terminal: ITerminal): void { From 2bfe74490a706f089a223d36d79e1a041f8c4984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 16:29:19 +0200 Subject: [PATCH 11/18] make .buffer local where appropriate --- src/InputHandler.ts | 180 +++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 70 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index acf7af1f..3936b33c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -23,6 +23,10 @@ export class InputHandler implements IInputHandler { public addChar(char: string, code: number): void { if (char >= ' ') { + + // make buffer local for faster access + let buffer = this._terminal.buffer; + // calculate print space // expensive call, therefore we save width in line buffer const chWidth = wcwidth(code); @@ -35,42 +39,42 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let row = buffer.y + buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps - if (!chWidth && this._terminal.buffer.x) { + if (!chWidth && buffer.x) { // dont overflow left - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { - if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (buffer.lines.get(row)[buffer.x - 1]) { + if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0); + if (buffer.lines.get(row)[buffer.x - 2]) { + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); } } else { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); } - this._terminal.updateRange(this._terminal.buffer.y); + this._terminal.updateRange(buffer.y); } return; } // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 - if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) { + if (buffer.x + chWidth - 1 >= this._terminal.cols) { // autowrap - DECAWM if (this._terminal.wraparoundMode) { - this._terminal.buffer.x = 0; - this._terminal.buffer.y++; - if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) { - this._terminal.buffer.y--; + buffer.x = 0; + buffer.y++; + if (buffer.y > buffer.scrollBottom) { + buffer.y--; this._terminal.scroll(true); } else { // The line already exists (eg. the initial viewport), mark it as a // wrapped line - (this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; + (buffer.lines.get(buffer.y)).isWrapped = true; } } else { if (chWidth === 2) { // FIXME: check for xterm behavior @@ -78,7 +82,7 @@ export class InputHandler implements IInputHandler { } } } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; + row = buffer.y + buffer.ybase; // insert mode: move characters to right if (this._terminal.insertMode) { @@ -86,26 +90,26 @@ export class InputHandler implements IInputHandler { for (let moves = 0; moves < chWidth; ++moves) { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well - const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); + const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && buffer.lines.get(row)[this._terminal.cols - 2] + && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; - this._terminal.buffer.x++; - this._terminal.updateRange(this._terminal.buffer.y); + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.x++; + this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; - this._terminal.buffer.x++; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + buffer.x++; } } } @@ -123,17 +127,20 @@ export class InputHandler implements IInputHandler { * Line Feed or New Line (NL). (LF is Ctrl-J). */ public lineFeed(): void { + // make buffer local for faster access + let buffer = this._terminal.buffer; + if (this._terminal.convertEol) { - this._terminal.buffer.x = 0; + buffer.x = 0; } - this._terminal.buffer.y++; - if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) { - this._terminal.buffer.y--; + buffer.y++; + if (buffer.y > buffer.scrollBottom) { + buffer.y--; this._terminal.scroll(); } // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x--; + if (buffer.x >= this._terminal.cols) { + buffer.x--; } /** * This event is emitted whenever the terminal outputs a LF or NL. @@ -199,13 +206,16 @@ export class InputHandler implements IInputHandler { let param = params[0]; if (param < 1) param = 1; - const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - let j = this._terminal.buffer.x; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; + let j = buffer.x; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param-- && j < this._terminal.cols) { - this._terminal.buffer.lines.get(row).splice(j++, 0, ch); - this._terminal.buffer.lines.get(row).pop(); + buffer.lines.get(row).splice(j++, 0, ch); + buffer.lines.get(row).pop(); } } @@ -447,20 +457,24 @@ export class InputHandler implements IInputHandler { if (param < 1) { param = 1; } - let row: number = this._terminal.buffer.y + this._terminal.buffer.ybase; - let scrollBottomRowsOffset = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; - let scrollBottomAbsolute = this._terminal.rows - 1 + this._terminal.buffer.ybase - scrollBottomRowsOffset + 1; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + let row: number = buffer.y + buffer.ybase; + + let scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom; + let scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; while (param--) { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior - this._terminal.buffer.lines.splice(scrollBottomAbsolute - 1, 1); - this._terminal.buffer.lines.splice(row, 0, this._terminal.blankLine(true)); + buffer.lines.splice(scrollBottomAbsolute - 1, 1); + buffer.lines.splice(row, 0, this._terminal.blankLine(true)); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.y); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.y); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -472,21 +486,25 @@ export class InputHandler implements IInputHandler { if (param < 1) { param = 1; } - const row: number = this._terminal.buffer.y + this._terminal.buffer.ybase; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row: number = buffer.y + buffer.ybase; let j: number; - j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; - j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j; + j = this._terminal.rows - 1 - buffer.scrollBottom; + j = this._terminal.rows - 1 + buffer.ybase - j; while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior - this._terminal.buffer.lines.splice(row, 1); - this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true)); + buffer.lines.splice(row, 1); + buffer.lines.splice(j, 0, this._terminal.blankLine(true)); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.y); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.y); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -499,14 +517,17 @@ export class InputHandler implements IInputHandler { param = 1; } - const row = this._terminal.buffer.y + this._terminal.buffer.ybase; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param--) { - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); - this._terminal.buffer.lines.get(row).push(ch); + buffer.lines.get(row).splice(buffer.x, 1); + buffer.lines.get(row).push(ch); } - this._terminal.updateRange(this._terminal.buffer.y); + this._terminal.updateRange(buffer.y); } /** @@ -514,13 +535,17 @@ export class InputHandler implements IInputHandler { */ public scrollUp(params: number[]): void { let param = params[0] || 1; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + while (param--) { - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 1); - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 0, this._terminal.blankLine()); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine()); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.scrollTop); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.scrollTop); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -528,13 +553,17 @@ export class InputHandler implements IInputHandler { */ public scrollDown(params: number[]): void { let param = params[0] || 1; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + while (param--) { - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 1); - this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 0, this._terminal.blankLine()); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); } // this.maxRange(); - this._terminal.updateRange(this._terminal.buffer.scrollTop); - this._terminal.updateRange(this._terminal.buffer.scrollBottom); + this._terminal.updateRange(buffer.scrollTop); + this._terminal.updateRange(buffer.scrollBottom); } /** @@ -547,12 +576,15 @@ export class InputHandler implements IInputHandler { param = 1; } - const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - let j = this._terminal.buffer.x; + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; + let j = buffer.x; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param-- && j < this._terminal.cols) { - this._terminal.buffer.lines.get(row)[j++] = ch; + buffer.lines.get(row)[j++] = ch; } } @@ -561,8 +593,12 @@ export class InputHandler implements IInputHandler { */ public cursorBackwardTab(params: number[]): void { let param = params[0] || 1; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + while (param--) { - this._terminal.buffer.x = this._terminal.buffer.prevStop(); + buffer.x = buffer.prevStop(); } } @@ -602,11 +638,15 @@ export class InputHandler implements IInputHandler { */ public repeatPrecedingCharacter(params: number[]): void { let param = params[0] || 1; - const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y); - const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; + + // make buffer local for faster access + let buffer = this._terminal.buffer; + + const line = buffer.lines.get(buffer.ybase + buffer.y); + const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; while (param--) { - line[this._terminal.buffer.x++] = ch; + line[buffer.x++] = ch; } } From 0decd77253202c2559805833b1ecc5e5df82b5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 16:59:55 +0200 Subject: [PATCH 12/18] map Buffer.lines directly to Buffer._lines --- src/Buffer.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index c1cf3cc7..0689c4cf 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -34,6 +34,7 @@ export class Buffer implements IBuffer { public savedY: number; public savedX: number; public markers: Marker[] = []; + public lines: CircularList; /** * Create a new Buffer. @@ -48,10 +49,6 @@ export class Buffer implements IBuffer { this.clear(); } - public get lines(): CircularList { - return this._lines; - } - public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -98,6 +95,7 @@ export class Buffer implements IBuffer { this.y = 0; this.x = 0; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = this._lines; this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); From dd096c2e8b681b49007124302d3b39fe6dcf9d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 17:19:42 +0200 Subject: [PATCH 13/18] const buffer --- src/InputHandler.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3936b33c..4df3e695 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -25,7 +25,7 @@ export class InputHandler implements IInputHandler { if (char >= ' ') { // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; // calculate print space // expensive call, therefore we save width in line buffer @@ -128,7 +128,7 @@ export class InputHandler implements IInputHandler { */ public lineFeed(): void { // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; if (this._terminal.convertEol) { buffer.x = 0; @@ -207,7 +207,7 @@ export class InputHandler implements IInputHandler { if (param < 1) param = 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; let j = buffer.x; @@ -459,7 +459,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; let row: number = buffer.y + buffer.ybase; @@ -488,7 +488,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row: number = buffer.y + buffer.ybase; @@ -518,7 +518,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm @@ -537,7 +537,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); @@ -555,7 +555,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); @@ -577,7 +577,7 @@ export class InputHandler implements IInputHandler { } // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; let j = buffer.x; @@ -595,7 +595,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; while (param--) { buffer.x = buffer.prevStop(); @@ -640,7 +640,7 @@ export class InputHandler implements IInputHandler { let param = params[0] || 1; // make buffer local for faster access - let buffer = this._terminal.buffer; + const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; From 0d7ecd912939cb699ad0fa2e48fc0b25e75f1101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 18:26:06 +0200 Subject: [PATCH 14/18] prefetch row in addChar --- src/InputHandler.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 4df3e695..db5032f9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -39,22 +39,23 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = buffer.y + buffer.ybase; + //let row = buffer.y + buffer.ybase; + let row = buffer.lines.get(buffer.y + buffer.ybase); // insert combining char in last cell // FIXME: needs handling after cursor jumps if (!chWidth && buffer.x) { // dont overflow left - if (buffer.lines.get(row)[buffer.x - 1]) { - if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (row[buffer.x - 1]) { + if (!row[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (buffer.lines.get(row)[buffer.x - 2]) { - buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); + if (row[buffer.x - 2]) { + row[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + row[buffer.x - 2][3] = char.charCodeAt(0); } } else { - buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); + row[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + row[buffer.x - 1][3] = char.charCodeAt(0); } this._terminal.updateRange(buffer.y); } @@ -82,7 +83,7 @@ export class InputHandler implements IInputHandler { } } } - row = buffer.y + buffer.ybase; + row = buffer.lines.get(buffer.y + buffer.ybase); // insert mode: move characters to right if (this._terminal.insertMode) { @@ -92,23 +93,23 @@ export class InputHandler implements IInputHandler { // we have to adjust the second last cell as well const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && buffer.lines.get(row)[this._terminal.cols - 2] - && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && row[this._terminal.cols - 2] + && row[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + row[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + row.splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + row[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; buffer.x++; this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + row[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; buffer.x++; } } From 13d4ab6f8dbd06564e7f479c906beea0956202f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 18:32:32 +0200 Subject: [PATCH 15/18] remove remnant --- src/InputHandler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index db5032f9..3d39e0c0 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -39,7 +39,6 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - //let row = buffer.y + buffer.ybase; let row = buffer.lines.get(buffer.y + buffer.ybase); // insert combining char in last cell From 6ff4ae7194b266ce0a6fb29bfac645be76f3b1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 22:06:24 +0200 Subject: [PATCH 16/18] revert to local const buffer --- src/Buffer.ts | 6 ++++-- src/InputHandler.ts | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 0689c4cf..c1cf3cc7 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -34,7 +34,6 @@ export class Buffer implements IBuffer { public savedY: number; public savedX: number; public markers: Marker[] = []; - public lines: CircularList; /** * Create a new Buffer. @@ -49,6 +48,10 @@ export class Buffer implements IBuffer { this.clear(); } + public get lines(): CircularList { + return this._lines; + } + public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -95,7 +98,6 @@ export class Buffer implements IBuffer { this.y = 0; this.x = 0; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); - this.lines = this._lines; this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3d39e0c0..4df3e695 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -39,22 +39,22 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = buffer.lines.get(buffer.y + buffer.ybase); + let row = buffer.y + buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps if (!chWidth && buffer.x) { // dont overflow left - if (row[buffer.x - 1]) { - if (!row[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (buffer.lines.get(row)[buffer.x - 1]) { + if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (row[buffer.x - 2]) { - row[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - row[buffer.x - 2][3] = char.charCodeAt(0); + if (buffer.lines.get(row)[buffer.x - 2]) { + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); } } else { - row[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - row[buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); } this._terminal.updateRange(buffer.y); } @@ -82,7 +82,7 @@ export class InputHandler implements IInputHandler { } } } - row = buffer.lines.get(buffer.y + buffer.ybase); + row = buffer.y + buffer.ybase; // insert mode: move characters to right if (this._terminal.insertMode) { @@ -92,23 +92,23 @@ export class InputHandler implements IInputHandler { // we have to adjust the second last cell as well const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && row[this._terminal.cols - 2] - && row[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - row[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && buffer.lines.get(row)[this._terminal.cols - 2] + && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - row.splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - row[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; buffer.x++; this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - row[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; buffer.x++; } } From 7ed7505ce47c91210c7c68cba7284d0de4f27a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 22:18:05 +0200 Subject: [PATCH 17/18] change _lines to lines in Buffer --- src/Buffer.ts | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index c1cf3cc7..1ea303b2 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -22,8 +22,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 * - scroll position */ export class Buffer implements IBuffer { - private _lines: CircularList; - + public lines: CircularList; public ydisp: number; public ybase: number; public y: number; @@ -48,10 +47,6 @@ export class Buffer implements IBuffer { this.clear(); } - public get lines(): CircularList { - return this._lines; - } - public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -81,7 +76,7 @@ export class Buffer implements IBuffer { * Fills the buffer's viewport with blank lines. */ public fillViewportRows(): void { - if (this._lines.length === 0) { + if (this.lines.length === 0) { let i = this._terminal.rows; while (i--) { this.lines.push(this._terminal.blankLine()); @@ -97,7 +92,7 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); @@ -112,19 +107,19 @@ export class Buffer implements IBuffer { // Increase max length if needed before adjustments to allow space to fill // as required. const newMaxLength = this._getCorrectBufferLength(newRows); - if (newMaxLength > this._lines.maxLength) { - this._lines.maxLength = newMaxLength; + if (newMaxLength > this.lines.maxLength) { + this.lines.maxLength = newMaxLength; } // The following adjustments should only happen if the buffer has been // initialized/filled. - if (this._lines.length > 0) { + if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? - for (let i = 0; i < this._lines.length; i++) { - while (this._lines.get(i).length < newCols) { - this._lines.get(i).push(ch); + for (let i = 0; i < this.lines.length; i++) { + while (this.lines.get(i).length < newCols) { + this.lines.get(i).push(ch); } } } @@ -133,8 +128,8 @@ export class Buffer implements IBuffer { let addToY = 0; if (this._terminal.rows < newRows) { for (let y = this._terminal.rows; y < newRows; y++) { - if (this._lines.length < newRows + this.ybase) { - if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) { + if (this.lines.length < newRows + this.ybase) { + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { // There is room above the buffer and there are no empty elements below the line, // scroll up this.ybase--; @@ -146,16 +141,16 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this._lines.push(this._terminal.blankLine(undefined, undefined, newCols)); + this.lines.push(this._terminal.blankLine(undefined, undefined, newCols)); } } } } else { // (this._terminal.rows >= newRows) for (let y = this._terminal.rows; y > newRows; y--) { - if (this._lines.length > newRows + this.ybase) { - if (this._lines.length > this.ybase + this.y + 1) { + if (this.lines.length > newRows + this.ybase) { + if (this.lines.length > this.ybase + this.y + 1) { // The line is a blank line below the cursor, remove it - this._lines.pop(); + this.lines.pop(); } else { // The line is the cursor, scroll down this.ybase++; @@ -167,15 +162,15 @@ export class Buffer implements IBuffer { // Reduce max length if needed after adjustments, this is done after as it // would otherwise cut data from the bottom of the buffer. - if (newMaxLength < this._lines.maxLength) { + if (newMaxLength < this.lines.maxLength) { // Trim from the top of the buffer and adjust ybase and ydisp. - const amountToTrim = this._lines.length - newMaxLength; + const amountToTrim = this.lines.length - newMaxLength; if (amountToTrim > 0) { - this._lines.trimStart(amountToTrim); + this.lines.trimStart(amountToTrim); this.ybase = Math.max(this.ybase - amountToTrim, 0); this.ydisp = Math.max(this.ydisp - amountToTrim, 0); } - this._lines.maxLength = newMaxLength; + this.lines.maxLength = newMaxLength; } // Make sure that the cursor stays on screen @@ -310,7 +305,7 @@ export class Buffer implements IBuffer { public addMarker(y: number): Marker { const marker = new Marker(y); this.markers.push(marker); - marker.disposables.push(this._lines.addDisposableListener('trim', amount => { + marker.disposables.push(this.lines.addDisposableListener('trim', amount => { marker.line -= amount; // The marker should be disposed when the line is trimmed from the buffer if (marker.line < 0) { From 9593cb0cc81eefcae9d0f5fbe546768d46073a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 23 Apr 2018 22:40:24 +0200 Subject: [PATCH 18/18] readonly lines; fix for MockBuffer --- src/Linkifier.test.ts | 2 +- src/Types.ts | 2 +- src/utils/TestUtils.test.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0e67403c..61245296 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -40,7 +40,7 @@ describe('Linkifier', () => { terminal = new MockTerminal(); terminal.cols = 100; terminal.buffer = new MockBuffer(); - terminal.buffer.lines = new CircularList(20); + (terminal.buffer).setLines(new CircularList(20)); terminal.buffer.ydisp = 0; linkifier = new TestLinkifier(terminal); mouseZoneManager = new TestMouseZoneManager(); diff --git a/src/Types.ts b/src/Types.ts index 84355e1f..2442b027 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -248,7 +248,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { } export interface IBuffer { - lines: ICircularList; + readonly lines: ICircularList; ydisp: number; ybase: number; y: number; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 4436c526..51aafb50 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -309,6 +309,9 @@ export class MockBuffer implements IBuffer { prevStop(x?: number): number { throw new Error('Method not implemented.'); } + setLines(lines: ICircularList<[number, string, number, number][]>): void { + this.lines = lines; + } } export class MockRenderer implements IRenderer {