diff --git a/README.md b/README.md index 2c450e42..5c4d2084 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file]( Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs. +## Releases + +Xterm.js follows a monthly release cycle roughly. + +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). + +### Beta builds + +Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: + +```bash +npm install -S xterm@beta +``` + +These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. + +## Contributing + +You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. + ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. @@ -197,26 +217,6 @@ Xterm.js is used in several world-class applications to provide great terminal e 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 on our list. Note: Please add any new contributions to the end of the list only. -## Releases - -Xterm.js follows a monthly release cycle roughly. - -All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). - -### Beta builds - -Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: - -```bash -npm install -S xterm@beta -``` - -These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. - -## Contributing - -You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. - ## License Agreement If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index b642efbc..726ad080 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -14,7 +14,7 @@ import { IColorSet, ILinkifier2 } from 'browser/Types'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services'; import { removeTerminalFromCache } from './atlas/CharAtlasCache'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { initEvent, EventEmitter, IEvent } from 'common/EventEmitter'; import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; let nextRendererId = 1; @@ -27,8 +27,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - private _onRequestRedraw = new EventEmitter(); - public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } + public readonly onRequestRedraw = initEvent(); constructor( private _colors: IColorSet, @@ -48,7 +47,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), - new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) + new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this.onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) ]; this.dimensions = { scaledCharWidth: 0, @@ -128,7 +127,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode)); // Selection foreground requires a full re-render if (this._colors.selectionForeground) { - this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); + this.onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } } @@ -201,6 +200,6 @@ export class CanvasRenderer extends Disposable implements IRenderer { } private _requestRedrawViewport(): void { - this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); + this.onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } } diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts index b0151e30..fa4be1e5 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts @@ -45,8 +45,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean a.allowTransparency === b.allowTransparency && a.scaledCharWidth === b.scaledCharWidth && a.scaledCharHeight === b.scaledCharHeight && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; + a.colors.foreground.rgba === b.colors.foreground.rgba && + a.colors.background.rgba === b.colors.background.rgba; } export function is256Color(colorCode: number): boolean { diff --git a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts index 3098538d..b809378e 100644 --- a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts @@ -96,7 +96,10 @@ export class DynamicCharAtlas extends BaseCharAtlas { const tmpCanvas = document.createElement('canvas'); tmpCanvas.width = this._config.scaledCharWidth; tmpCanvas.height = this._config.scaledCharHeight; - this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); + this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { + alpha: this._config.allowTransparency, + willReadFrequently: true + })); this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 689899ef..d14e1fe7 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -4,7 +4,7 @@ */ import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm'; -import { EventEmitter } from 'common/EventEmitter'; +import { initEvent } from 'common/EventEmitter'; export interface ISearchOptions { regex?: boolean; @@ -72,8 +72,7 @@ export class SearchAddon implements ITerminalAddon { private _resultIndex: number | undefined; - private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>(); - public readonly onDidChangeResults = this._onDidChangeResults.event; + public readonly onDidChangeResults = initEvent<{ resultIndex: number, resultCount: number } | undefined>(); public activate(terminal: Terminal): void { this._terminal = terminal; @@ -89,7 +88,7 @@ export class SearchAddon implements ITerminalAddon { this._highlightTimeout = setTimeout(() => { this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true }); this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1; - this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 }); + this.onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 }); }, 200); } } @@ -325,9 +324,9 @@ export class SearchAddon implements ITerminalAddon { private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean { if (searchOptions?.decorations) { if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) { - this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size }); + this.onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size }); } else { - this._onDidChangeResults.fire(undefined); + this.onDidChangeResults.fire(undefined); } } this._cachedSearchTerm = term; diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index 05f2c61c..df4a72da 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -78,7 +78,7 @@ describe('xterm-addon-serialize', () => { terminal.loadAddon(serializeAddon); selectionService = new TestSelectionService((terminal as any)._core._bufferService); - cm = new ColorManager(document, false); + cm = new ColorManager(); (terminal as any)._core._colorManager = cm; (terminal as any)._core._selectionService = selectionService; }); diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index a7f4a700..689b847a 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -70,13 +70,11 @@ const INDICES_PER_CELL = 10; const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; -/** Work variables to avoid garbage collection. */ -const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = { - i: 0, - glyph: undefined, - leftCellPadding: 0, - clippedPixels: 0 -}; +// Work variables to avoid garbage collection +let $i = 0; +let $glyph: IRasterizedGlyph | undefined = undefined; +let $leftCellPadding = 0; +let $clippedPixels = 0; export class GlyphRenderer extends Disposable { private _atlas: WebglCharAtlas | undefined; @@ -186,12 +184,12 @@ export class GlyphRenderer extends Disposable { } private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { - w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL; + $i = (y * this._terminal.cols + x) * INDICES_PER_CELL; // Exit early if this is a null character, allow space character to continue as it may have // underline/strikethrough styles if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) { - fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); + fill(array, 0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); return; } @@ -201,39 +199,39 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { - w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); + $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { - w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); + $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } - w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); - if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) { - w.clippedPixels = w.glyph.offset.x - w.leftCellPadding; + $leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); + if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { + $clippedPixels = $glyph.offset.x - $leftCellPadding; // a_origin - array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft; - array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop; + array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.scaledCharLeft; + array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop; // a_size - array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth; - array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight; + array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.scaledCanvasWidth; + array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight; // a_texcoord - array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width; - array[w.i + 5] = w.glyph.texturePositionClipSpace.y; + array[$i + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 5] = $glyph.texturePositionClipSpace.y; // a_texsize - array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width; - array[w.i + 7] = w.glyph.sizeClipSpace.y; + array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 7] = $glyph.sizeClipSpace.y; } else { // a_origin - array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft; - array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop; + array[$i ] = -$glyph.offset.x + this._dimensions.scaledCharLeft; + array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop; // a_size - array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth; - array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight; + array[$i + 2] = $glyph.size.x / this._dimensions.scaledCanvasWidth; + array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight; // a_texcoord - array[w.i + 4] = w.glyph.texturePositionClipSpace.x; - array[w.i + 5] = w.glyph.texturePositionClipSpace.y; + array[$i + 4] = $glyph.texturePositionClipSpace.x; + array[$i + 5] = $glyph.texturePositionClipSpace.y; // a_texsize - array[w.i + 6] = w.glyph.sizeClipSpace.x; - array[w.i + 7] = w.glyph.sizeClipSpace.y; + array[$i + 6] = $glyph.sizeClipSpace.x; + array[$i + 7] = $glyph.sizeClipSpace.y; } // a_cellpos only changes on resize } diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index ef08fb7c..dccc7f6a 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -58,17 +58,15 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE; -/** Work variables to avoid garbage collection. */ -const w: { rgba: number, isDefault: boolean, x1: number, y1: number, r: number, g: number, b: number, a: number } = { - rgba: 0, - isDefault: false, - x1: 0, - y1: 0, - r: 0, - g: 0, - b: 0, - a: 0 -}; +// Work variables to avoid garbage collection +let $rgba = 0; +let $isDefault = false; +let $x1 = 0; +let $y1 = 0; +let $r = 0; +let $g = 0; +let $b = 0; +let $a = 0; export class RectangleRenderer extends Disposable { @@ -232,47 +230,47 @@ export class RectangleRenderer extends Disposable { } private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { - w.isDefault = false; + $isDefault = false; if (fg & FgFlags.INVERSE) { switch (fg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: - w.rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + $rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - w.rgba = (fg & Attributes.RGB_MASK) << 8; + $rgba = (fg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: - w.rgba = this._colors.foreground.rgba; + $rgba = this._colors.foreground.rgba; } } else { switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: - w.rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; + $rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - w.rgba = (bg & Attributes.RGB_MASK) << 8; + $rgba = (bg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: - w.rgba = this._colors.background.rgba; - w.isDefault = true; + $rgba = this._colors.background.rgba; + $isDefault = true; } } if (vertices.attributes.length < offset + 4) { vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); } - w.x1 = startX * this._dimensions.scaledCellWidth; - w.y1 = y * this._dimensions.scaledCellHeight; - w.r = ((w.rgba >> 24) & 0xFF) / 255; - w.g = ((w.rgba >> 16) & 0xFF) / 255; - w.b = ((w.rgba >> 8 ) & 0xFF) / 255; - w.a = (!w.isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1; + $x1 = startX * this._dimensions.scaledCellWidth; + $y1 = y * this._dimensions.scaledCellHeight; + $r = (($rgba >> 24) & 0xFF) / 255; + $g = (($rgba >> 16) & 0xFF) / 255; + $b = (($rgba >> 8 ) & 0xFF) / 255; + $a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1; - this._addRectangle(vertices.attributes, offset, w.x1, w.y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, w.r, w.g, w.b, w.a); + this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, $r, $g, $b, $a); } private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void { diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 5b98a048..b26c565c 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -7,7 +7,7 @@ import { Terminal, ITerminalAddon, IEvent } from 'xterm'; import { WebglRenderer } from './WebglRenderer'; import { ICharacterJoinerService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; -import { EventEmitter, forwardEvent } from 'common/EventEmitter'; +import { EventEmitter, forwardEvent, initEvent } from 'common/EventEmitter'; import { isSafari } from 'common/Platform'; import { ICoreService, IDecorationService } from 'common/services/Services'; @@ -15,14 +15,12 @@ export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private _onChangeTextureAtlas = new EventEmitter(); - public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } - private _onContextLoss = new EventEmitter(); - public get onContextLoss(): IEvent { return this._onContextLoss.event; } + public readonly onChangeTextureAtlas = initEvent(); + public readonly onContextLoss = initEvent(); constructor( private _preserveDrawingBuffer?: boolean - ) {} + ) { } public activate(terminal: Terminal): void { if (!terminal.element) { @@ -39,8 +37,8 @@ export class WebglAddon implements ITerminalAddon { const decorationService: IDecorationService = (terminal as any)._core._decorationService; const colors: IColorSet = (terminal as any)._core._colorManager.colors; this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer); - forwardEvent(this._renderer.onContextLoss, this._onContextLoss); - forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas); + forwardEvent(this._renderer.onContextLoss, this.onContextLoss); + forwardEvent(this._renderer.onChangeTextureAtlas, this.onChangeTextureAtlas); renderService.setRenderer(this._renderer); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index d8daa0d1..8d8b24f7 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -18,7 +18,7 @@ import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; import { ITerminal, IColorSet } from 'browser/Types'; -import { EventEmitter } from 'common/EventEmitter'; +import { EventEmitter, initEvent } from 'common/EventEmitter'; import { CellData } from 'common/buffer/CellData'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; @@ -26,14 +26,12 @@ import { CharData, IBufferLine, ICellData } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { ICoreService, IDecorationService } from 'common/services/Services'; -/** Work variables to avoid garbage collection. */ -const w: { fg: number, bg: number, hasFg: boolean, hasBg: boolean, isSelected: boolean } = { - fg: 0, - bg: 0, - hasFg: false, - hasBg: false, - isSelected: false -}; +// Work variables to avoid garbage collection +let $fg = 0; +let $bg = 0; +let $hasFg = false; +let $hasBg = false; +let $isSelected = false; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -55,13 +53,9 @@ export class WebglRenderer extends Disposable implements IRenderer { private _isAttached: boolean; private _contextRestorationTimeout: number | undefined; - private _onChangeTextureAtlas = new EventEmitter(); - public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } - private _onRequestRedraw = new EventEmitter(); - public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } - - private _onContextLoss = new EventEmitter(); - public get onContextLoss(): IEvent { return this._onContextLoss.event; } + public readonly onChangeTextureAtlas = initEvent(); + public readonly onRequestRedraw = initEvent(); + public readonly onContextLoss = initEvent(); constructor( private _terminal: Terminal, @@ -78,7 +72,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._renderLayers = [ new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core, this._coreBrowserService), - new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._onRequestRedraw, this._coreBrowserService, coreService) + new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this.onRequestRedraw, this._coreBrowserService, coreService) ]; this.dimensions = { scaledCharWidth: 0, @@ -118,7 +112,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._contextRestorationTimeout = setTimeout(() => { this._contextRestorationTimeout = undefined; console.warn('webgl context not restored; firing onContextLoss'); - this._onContextLoss.fire(e); + this.onContextLoss.fire(e); }, 3000 /* ms */); })); this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => { @@ -286,7 +280,7 @@ export class WebglRenderer extends Disposable implements IRenderer { throw new Error('The webgl renderer only works with the webgl char atlas'); } if (this._charAtlas !== atlas) { - this._onChangeTextureAtlas.fire(atlas.cacheCanvas); + this.onChangeTextureAtlas.fire(atlas.cacheCanvas); } this._charAtlas = atlas; this._charAtlas.warmUp(); @@ -425,9 +419,9 @@ export class WebglRenderer extends Disposable implements IRenderer { // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg && - this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg && + this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) { continue; } @@ -475,89 +469,89 @@ export class WebglRenderer extends Disposable implements IRenderer { // override logic throughout the different sub-renderers // Reset overrides work variables - w.bg = 0; - w.fg = 0; - w.hasBg = false; - w.hasFg = false; - w.isSelected = false; + $bg = 0; + $fg = 0; + $hasBg = false; + $hasFg = false; + $isSelected = false; // Apply decorations on the bottom layer this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { if (d.backgroundColorRGB) { - w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasBg = true; + $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasBg = true; } if (d.foregroundColorRGB) { - w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasFg = true; + $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasFg = true; } }); // Apply the selection color if needed - w.isSelected = this._isCellSelected(x, y); - if (w.isSelected) { - w.bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; - w.hasBg = true; + $isSelected = this._isCellSelected(x, y); + if ($isSelected) { + $bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; + $hasBg = true; if (this._colors.selectionForeground) { - w.fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; - w.hasFg = true; + $fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + $hasFg = true; } } // Apply decorations on the top layer this._decorationService.forEachDecorationAtCell(x, y, 'top', d => { if (d.backgroundColorRGB) { - w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasBg = true; + $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasBg = true; } if (d.foregroundColorRGB) { - w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasFg = true; + $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasFg = true; } }); // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag // ahead of time in order to use the correct cache key - if (w.hasBg) { - if (w.isSelected) { + if ($hasBg) { + if ($isSelected) { // Non-RGB attributes from model + force non-dim + override + force RGB color mode - w.bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | w.bg | Attributes.CM_RGB; + $bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB; } else { // Non-RGB attributes from model + override + force RGB color mode - w.bg = (this._workCell.bg & ~Attributes.RGB_MASK) | w.bg | Attributes.CM_RGB; + $bg = (this._workCell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB; } } - if (w.hasFg) { + if ($hasFg) { // Non-RGB attributes from model + force disable inverse + override + force RGB color mode - w.fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | w.fg | Attributes.CM_RGB; + $fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB; } // Handle case where inverse was specified by only one of bg override or fg override was set, // resolving the other inverse color and setting the inverse flag if needed. if (this._workColors.fg & FgFlags.INVERSE) { - if (w.hasBg && !w.hasFg) { + if ($hasBg && !$hasFg) { // Resolve bg color type (default color has a different meaning in fg vs bg) if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + $fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; } else { - w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); + $fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); } - w.hasFg = true; + $hasFg = true; } - if (!w.hasBg && w.hasFg) { + if (!$hasBg && $hasFg) { // Resolve bg color type (default color has a different meaning in fg vs bg) if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + $bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; } else { - w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); + $bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); } - w.hasBg = true; + $hasBg = true; } } // Use the override if it exists - this._workColors.bg = w.hasBg ? w.bg : this._workColors.bg; - this._workColors.fg = w.hasFg ? w.fg : this._workColors.fg; + this._workColors.bg = $hasBg ? $bg : this._workColors.bg; + this._workColors.fg = $hasFg ? $fg : this._workColors.fg; } private _isCellSelected(x: number, y: number): boolean { @@ -679,7 +673,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } private _requestRedrawViewport(): void { - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this.onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); } } diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 83f82fa7..dc503d75 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -70,8 +70,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean a.scaledCharHeight === b.scaledCharHeight && a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors && a.minimumContrastRatio === b.minimumContrastRatio && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; + a.colors.foreground.rgba === b.colors.foreground.rgba && + a.colors.background.rgba === b.colors.background.rgba; } export function is256Color(colorCode: number): boolean { diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b4593d90..dee1e02c 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -16,6 +16,7 @@ import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; import { FourKeyMap } from 'common/MultiKeyMap'; +import { IdleTaskQueue } from 'common/TaskQueue'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. @@ -53,10 +54,8 @@ interface ICharAtlasActiveRow { height: number; } -/** Work variables to avoid garbage collection. */ -const w: { glyph: IRasterizedGlyph | undefined } = { - glyph: undefined -}; +// Work variables to avoid garbage collection +let $glyph = undefined; export class WebglCharAtlas implements IDisposable { private _didWarmUp: boolean = false; @@ -110,7 +109,10 @@ export class WebglCharAtlas implements IDisposable { this._tmpCanvas = document.createElement('canvas'); this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; - this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); + this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { + alpha: this._config.allowTransparency, + willReadFrequently: true + })); } public dispose(): void { @@ -127,10 +129,15 @@ export class WebglCharAtlas implements IDisposable { } private _doWarmUp(): void { - // Pre-fill with ASCII 33-126 + // Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks + const queue = new IdleTaskQueue(); for (let i = 33; i < 126; i++) { - const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); - this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + queue.enqueue(() => { + if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) { + const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); + this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + } + }); } } @@ -175,12 +182,12 @@ export class WebglCharAtlas implements IDisposable { fg: number, ext: number ): IRasterizedGlyph { - w.glyph = cacheMap.get(key, bg, fg, ext); - if (!w.glyph) { - w.glyph = this._drawToCache(key, bg, fg, ext); - cacheMap.set(key, bg, fg, ext, w.glyph); + $glyph = cacheMap.get(key, bg, fg, ext); + if (!$glyph) { + $glyph = this._drawToCache(key, bg, fg, ext); + cacheMap.set(key, bg, fg, ext, $glyph); } - return w.glyph; + return $glyph; } private _getColorFromAnsiIndex(idx: number): IColor { diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index 74aed0cc..6865b6db 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -15,12 +15,12 @@ declare module 'xterm-addon-webgl' { /** * An event that is fired when the renderer loses its canvas context. */ - public get onContextLoss(): IEvent; + public readonly onContextLoss: IEvent; /** * An event that is fired when the texture atlas of the renderer changes. */ - public get onChangeTextureAtlas(): IEvent; + public readonly onChangeTextureAtlas: IEvent; constructor(preserveDrawingBuffer?: boolean); diff --git a/demo/server.js b/demo/server.js index 8d295942..0e82f9e9 100644 --- a/demo/server.js +++ b/demo/server.js @@ -16,8 +16,7 @@ function startServer() { var app = express(); expressWs(app); - var terminals = {}, - logs = {}; + var terminals = {}; app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css')); app.get('/logo.png', (req, res) => { @@ -55,10 +54,6 @@ function startServer() { console.log('Created terminal with PID: ' + term.pid); terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; - }); res.send(term.pid.toString()); res.end(); }); @@ -77,15 +72,25 @@ function startServer() { app.ws('/terminals/:pid', function (ws, req) { var term = terminals[parseInt(req.params.pid)]; console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); + + // unbuffered delivery after user input + let userInput = false; // string message buffering - function buffer(socket, timeout) { + function buffer(socket, timeout, maxSize) { let s = ''; let sender = null; return (data) => { s += data; - if (!sender) { + if (s.length > maxSize || userInput) { + userInput = false; + socket.send(s); + s = ''; + if (sender) { + clearTimeout(sender); + sender = null; + } + } else if (!sender) { sender = setTimeout(() => { socket.send(s); s = ''; @@ -95,14 +100,23 @@ function startServer() { }; } // binary message buffering - function bufferUtf8(socket, timeout) { + function bufferUtf8(socket, timeout, maxSize) { let buffer = []; let sender = null; let length = 0; return (data) => { buffer.push(data); length += data.length; - if (!sender) { + if (length > maxSize || userInput) { + userInput = false; + socket.send(Buffer.concat(buffer, length)); + buffer = []; + length = 0; + if (sender) { + clearTimeout(sender); + sender = null; + } + } else if (!sender) { sender = setTimeout(() => { socket.send(Buffer.concat(buffer, length)); buffer = []; @@ -112,27 +126,23 @@ function startServer() { } }; } - const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5); + const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144); // WARNING: This is a naive implementation that will not throttle the flow of data. This means // it could flood the communication channel and make the terminal unresponsive. Learn more about // the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/ term.on('data', function(data) { - try { - send(data); - } catch (ex) { - // The WebSocket is not open, ignore - } + send(data); }); ws.on('message', function(msg) { term.write(msg); + userInput = true; }); ws.on('close', function () { term.kill(); console.log('Closed terminal ' + term.pid); // Clean things up delete terminals[term.pid]; - delete logs[term.pid]; }); }); diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index 019bf42a..cf60a1f5 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -28,7 +28,7 @@ describe('ColorManager', () => { return {data: [0, 0, 0, 0xFF]}; } }); - cm = new ColorManager(document, false); + cm = new ColorManager(); }); describe('constructor', () => { diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index d8dcde6f..a22a423f 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -80,22 +80,11 @@ export const DEFAULT_ANSI_COLORS = Object.freeze((() => { */ export class ColorManager implements IColorManager { public colors: IColorSet; - private _ctx: CanvasRenderingContext2D; - private _litmusColor: CanvasGradient; + private _contrastCache: IColorContrastCache; private _restoreColors!: IRestoreColorSet; - constructor(document: Document, public allowTransparency: boolean) { - const canvas = document.createElement('canvas'); - canvas.width = 1; - canvas.height = 1; - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Could not get rendering context'); - } - this._ctx = ctx; - this._ctx.globalCompositeOperation = 'copy'; - this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); + constructor() { this._contrastCache = new ColorContrastCache(); this.colors = { foreground: DEFAULT_FOREGROUND, @@ -118,9 +107,6 @@ export class ColorManager implements IColorManager { case 'minimumContrastRatio': this._contrastCache.clear(); break; - case 'allowTransparency': - this.allowTransparency = value; - break; } } @@ -132,11 +118,11 @@ export class ColorManager implements IColorManager { public setTheme(theme: ITheme = {}): void { this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND); this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND); - this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true); - this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); - this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION, true); + this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR); + this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION); this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent); - this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent, true); + this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent); this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent); const nullColor: IColor = { css: '', @@ -220,69 +206,16 @@ export class ColorManager implements IColorManager { } private _parseColor( - css: string | undefined, - fallback: IColor, - allowTransparency: boolean = this.allowTransparency + cssString: string | undefined, + fallback: IColor ): IColor { - if (css === undefined) { - return fallback; - } - - // If parsing the value results in failure, then it must be ignored, and the attribute must - // retain its previous value. - // -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles - this._ctx.fillStyle = this._litmusColor; - this._ctx.fillStyle = css; - if (typeof this._ctx.fillStyle !== 'string') { - console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`); - return fallback; - } - - this._ctx.fillRect(0, 0, 1, 1); - const data = this._ctx.getImageData(0, 0, 1, 1).data; - - // Check if the printed color was transparent - if (data[3] !== 0xFF) { - if (!allowTransparency) { - // Ideally we'd just ignore the alpha channel, but... - // - // Browsers may not give back exactly the same RGB values we put in, because most/all - // convert the color to a pre-multiplied representation. getImageData converts that back to - // a un-premultipled representation, but the precision loss may make the RGB channels unuable - // on their own. - // - // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns - // into 0x00000000. - // - // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, - // pixels that have just been set using putImageData() might be returned to an equivalent - // getImageData() as different values." - // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation - // - // So let's just use the fallback color in this case instead. - console.warn( - `Color: ${css} is using transparency, but allowTransparency is false. ` + - `Using fallback ${fallback.css}.` - ); - return fallback; + if (cssString !== undefined) { + try { + return css.toColor(cssString); + } catch { + // no-op } - - // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color - // the color value has alpha less than 1.0, and the string is the color value in the CSS rgba() - const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component)); - const alpha = Math.round(a * 255); - const rgba: number = channels.toRgba(r, g, b, alpha); - return { - rgba, - css - }; } - - return { - // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color - // if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character - css: this._ctx.fillStyle, - rgba: channels.toRgba(data[0], data[1], data[2], data[3]) - }; + return fallback; } } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 9c978949..844b64f8 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -7,7 +7,7 @@ import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent import { IDisposable } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBufferService } from 'common/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; @@ -26,10 +26,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { private _activeProviderReplies: Map | undefined; private _activeLine: number = -1; - private _onShowLinkUnderline = this.register(new EventEmitter()); - public get onShowLinkUnderline(): IEvent { return this._onShowLinkUnderline.event; } - private _onHideLinkUnderline = this.register(new EventEmitter()); - public get onHideLinkUnderline(): IEvent { return this._onHideLinkUnderline.event; } + public readonly onShowLinkUnderline = this.register(initEvent()); + public readonly onHideLinkUnderline = this.register(initEvent()); constructor( @IBufferService private readonly _bufferService: IBufferService @@ -343,7 +341,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { const range = link.range; const scrollOffset = this._bufferService.buffer.ydisp; const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined); - const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline; + const emitter = showEvent ? this.onShowLinkUnderline : this.onHideLinkUnderline; emitter.fire(event); } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 9aba3818..90c1b134 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -37,7 +37,7 @@ import { ITheme, IMarker, IDisposable, ILinkProvider, IDecorationOptions, IDecor import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; -import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, forwardEvent, initEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; @@ -122,27 +122,16 @@ export class Terminal extends CoreTerminal implements ITerminal { private _colorManager: ColorManager | undefined; private _theme: ITheme | undefined; - private _onCursorMove = new EventEmitter(); - public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); - public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } - private _onRender = new EventEmitter<{ start: number, end: number }>(); - public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onSelectionChange = new EventEmitter(); - public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } - private _onTitleChange = new EventEmitter(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - private _onBell = new EventEmitter(); - public get onBell(): IEvent { return this._onBell.event; } - - private _onFocus = new EventEmitter(); - public get onFocus(): IEvent { return this._onFocus.event; } - private _onBlur = new EventEmitter(); - public get onBlur(): IEvent { return this._onBlur.event; } - private _onA11yCharEmitter = new EventEmitter(); - public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } - private _onA11yTabEmitter = new EventEmitter(); - public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } + public readonly onCursorMove = initEvent(); + public readonly onKey = initEvent<{ key: string, domEvent: KeyboardEvent }>(); + public readonly onRender = initEvent<{ start: number, end: number }>(); + public readonly onSelectionChange = initEvent(); + public readonly onTitleChange = initEvent(); + public readonly onBell = initEvent(); + public readonly onFocus = initEvent(); + public readonly onBlur = initEvent(); + public readonly onA11yChar = initEvent(); + public readonly onA11yTab = initEvent(); /** * Creates a new `Terminal` object. @@ -169,16 +158,16 @@ export class Terminal extends CoreTerminal implements ITerminal { this._instantiationService.setService(IDecorationService, this._decorationService); // Setup InputHandler listeners - this.register(this._inputHandler.onRequestBell(() => this._onBell.fire())); + this.register(this._inputHandler.onRequestBell(() => this.onBell.fire())); this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); this.register(this._inputHandler.onRequestSendFocus(() => this._reportFocus())); this.register(this._inputHandler.onRequestReset(() => this.reset())); this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); this.register(this._inputHandler.onColor((event) => this._handleColorEvent(event))); - this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); - this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); - this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); - this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + this.register(forwardEvent(this._inputHandler.onCursorMove, this.onCursorMove)); + this.register(forwardEvent(this._inputHandler.onTitleChange, this.onTitleChange)); + this.register(forwardEvent(this._inputHandler.onA11yChar, this.onA11yChar)); + this.register(forwardEvent(this._inputHandler.onA11yTab, this.onA11yTab)); // Setup listeners this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); @@ -326,7 +315,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.updateCursorStyle(ev); this.element!.classList.add('focus'); this._showCursor(); - this._onFocus.fire(); + this.onFocus.fire(); } /** @@ -349,7 +338,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.coreService.triggerDataEvent(C0.ESC + '[O'); } this.element!.classList.remove('focus'); - this._onBlur.fire(); + this.onBlur.fire(); } private _syncTextArea(): void { @@ -502,7 +491,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._instantiationService.setService(ICharSizeService, this._charSizeService); this._theme = this.options.theme || this._theme; - this._colorManager = new ColorManager(document, this.options.allowTransparency); + this._colorManager = new ColorManager(); this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e]))); this._colorManager.setTheme(this._theme); @@ -512,7 +501,7 @@ export class Terminal extends CoreTerminal implements ITerminal { const renderer = this._createRenderer(); this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); this._instantiationService.setService(IRenderService, this._renderService); - this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e))); + this.register(this._renderService.onRenderedViewportChange(e => this.onRender.fire(e))); this.onResize(e => this._renderService!.resize(e.cols, e.rows)); this._compositionView = document.createElement('div'); @@ -552,7 +541,7 @@ export class Terminal extends CoreTerminal implements ITerminal { )); this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent))); - this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); + this.register(this._selectionService.onSelectionChange(() => this.onSelectionChange.fire())); this.register(this._selectionService.onRequestRedraw(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this._selectionService.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it @@ -1101,7 +1090,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.textarea!.value = ''; } - this._onKey.fire({ key: result.key, domEvent: event }); + this.onKey.fire({ key: result.key, domEvent: event }); this._showCursor(); this.coreService.triggerDataEvent(result.key, true); @@ -1184,7 +1173,7 @@ export class Terminal extends CoreTerminal implements ITerminal { key = String.fromCharCode(key); - this._onKey.fire({ key, domEvent: ev }); + this.onKey.fire({ key, domEvent: ev }); this._showCursor(); this.coreService.triggerDataEvent(key, true); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 0b5e00c1..95920375 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -4,7 +4,7 @@ */ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { IEvent, EventEmitter, initEvent } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; @@ -352,7 +352,7 @@ export class MockCoreBrowserService implements ICoreBrowserService { export class MockCharSizeService implements ICharSizeService { public serviceBrand: undefined; public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } - public onCharSizeChange: IEvent = new EventEmitter().event; + public onCharSizeChange: IEvent = initEvent(); constructor(public width: number, public height: number) {} public measure(): void {} } @@ -370,10 +370,10 @@ export class MockMouseService implements IMouseService { export class MockRenderService implements IRenderService { public serviceBrand: undefined; - public onDimensionsChange: IEvent = new EventEmitter().event; - public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; - public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; - public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event; + public onDimensionsChange: IEvent = initEvent(); + public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = initEvent<{ start: number, end: number }>(); + public onRender: IEvent<{ start: number, end: number }, void> = initEvent<{ start: number, end: number }>(); + public onRefreshRequest: IEvent<{ start: number, end: number}, void> = initEvent<{ start: number, end: number }>(); public dimensions: IRenderDimensions = { scaledCharWidth: 0, scaledCharHeight: 0, @@ -457,10 +457,10 @@ export class MockSelectionService implements ISelectionService { public hasSelection: boolean = false; public selectionStart: [number, number] | undefined; public selectionEnd: [number, number] | undefined; - public onLinuxMouseSelection = new EventEmitter().event; - public onRequestRedraw = new EventEmitter().event; - public onRequestScrollLines = new EventEmitter().event; - public onSelectionChange = new EventEmitter().event; + public onLinuxMouseSelection = initEvent(); + public onRequestRedraw = initEvent(); + public onRequestScrollLines = initEvent(); + public onSelectionChange = initEvent(); public disable(): void { throw new Error('Method not implemented.'); } diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index 7fcc5ea9..23ba174f 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -95,7 +95,7 @@ export class BufferDecorationRenderer extends Disposable { // outside of viewport if (decoration.element) { decoration.element.style.display = 'none'; - decoration.onRenderEmitter.fire(decoration.element); + decoration.onRender.fire(decoration.element); } } else { let element = this._decorationElements.get(decoration); @@ -108,7 +108,7 @@ export class BufferDecorationRenderer extends Disposable { } element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`; element.style.display = this._altBufferIsActive ? 'none' : 'block'; - decoration.onRenderEmitter.fire(element); + decoration.onRender.fire(element); } } diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts index 4c3874c7..32256cf4 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/CustomGlyphs.ts @@ -33,7 +33,7 @@ export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefi '▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK // Block elements (0x2594-0x2595) - '▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK + '▔': [{ x: 0, y: 0, w: 8, h: 1 }], // UPPER ONE EIGHTH BLOCK '▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK // Terminal graphic characters (0x2596-0x259F) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 9dd5154c..8900a157 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -10,7 +10,7 @@ import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { color } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; @@ -40,7 +40,7 @@ export class DomRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - public get onRequestRedraw(): IEvent { return new EventEmitter().event; } + public readonly onRequestRedraw = initEvent(); constructor( private _colors: IColorSet, diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index d3eb9e8e..b6ee7bf1 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -166,7 +166,7 @@ export class DomRendererRowFactory { if (cell.isUnderline()) { charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`); if (charElement.textContent === ' ') { - charElement.innerHTML = ' '; + charElement.textContent = '\xa0'; // =   } if (!cell.isUnderlineColorDefault()) { if (cell.isUnderlineColorRGB()) { diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index b04e157f..583006d6 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -4,7 +4,7 @@ */ import { IOptionsService } from 'common/services/Services'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { IEvent, EventEmitter, initEvent } from 'common/EventEmitter'; import { ICharSizeService } from 'browser/services/Services'; export class CharSizeService implements ICharSizeService { @@ -16,8 +16,7 @@ export class CharSizeService implements ICharSizeService { public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } - private _onCharSizeChange = new EventEmitter(); - public get onCharSizeChange(): IEvent { return this._onCharSizeChange.event; } + public readonly onCharSizeChange = initEvent(); constructor( document: Document, @@ -32,7 +31,7 @@ export class CharSizeService implements ICharSizeService { if (result.width !== this.width || result.height !== this.height) { this.width = result.width; this.height = result.height; - this._onCharSizeChange.fire(); + this.onCharSizeChange.fire(); } } } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 7db0e5cd..dfb4df80 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -5,13 +5,14 @@ import { IRenderer, IRenderDimensions } from 'browser/renderer/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { DebouncedIdleTask } from 'common/TaskQueue'; interface ISelectionState { start: [number, number] | undefined; @@ -24,6 +25,7 @@ export class RenderService extends Disposable implements IRenderService { private _renderDebouncer: IRenderDebouncerWithCallback; private _screenDprMonitor: ScreenDprMonitor; + private _pausedResizeTask = new DebouncedIdleTask(); private _isPaused: boolean = false; private _needsFullRefresh: boolean = false; @@ -37,14 +39,10 @@ export class RenderService extends Disposable implements IRenderService { columnSelectMode: false }; - private _onDimensionsChange = new EventEmitter(); - public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } - private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>(); - public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; } - private _onRender = new EventEmitter<{ start: number, end: number }>(); - public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); - public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } + public readonly onDimensionsChange = initEvent(); + public readonly onRenderedViewportChange = initEvent<{ start: number, end: number }>(); + public readonly onRender = initEvent<{ start: number, end: number }>(); + public readonly onRefreshRequest = initEvent<{ start: number, end: number }>(); public get dimensions(): IRenderDimensions { return this._renderer.dimensions; } @@ -105,6 +103,7 @@ export class RenderService extends Disposable implements IRenderService { } if (!this._isPaused && this._needsFullRefresh) { + this._pausedResizeTask.flush(); this.refreshRows(0, this._rowCount - 1); this._needsFullRefresh = false; } @@ -132,9 +131,9 @@ export class RenderService extends Disposable implements IRenderService { // Fire render event only if it was not a redraw if (!this._isNextRenderRedrawOnly) { - this._onRenderedViewportChange.fire({ start, end }); + this.onRenderedViewportChange.fire({ start, end }); } - this._onRender.fire({ start, end }); + this.onRender.fire({ start, end }); this._isNextRenderRedrawOnly = true; } @@ -154,7 +153,7 @@ export class RenderService extends Disposable implements IRenderService { if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) { return; } - this._onDimensionsChange.fire(this._renderer.dimensions); + this.onDimensionsChange.fire(this._renderer.dimensions); } public dispose(): void { @@ -204,7 +203,11 @@ export class RenderService extends Disposable implements IRenderService { } public onResize(cols: number, rows: number): void { - this._renderer.onResize(cols, rows); + if (this._isPaused) { + this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows)); + } else { + this._renderer.onResize(cols, rows); + } this._fullRefresh(); } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 4ee1ffa1..5da197b1 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -9,7 +9,7 @@ import { IBufferLine, IDisposable } from 'common/Types'; import * as Browser from 'common/Platform'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { CellData } from 'common/buffer/CellData'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { IMouseService, ISelectionService, IRenderService, ICoreBrowserService } from 'browser/services/Services'; import { IBufferRange, ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; @@ -111,14 +111,10 @@ export class SelectionService extends Disposable implements ISelectionService { private _oldSelectionStart: [number, number] | undefined = undefined; private _oldSelectionEnd: [number, number] | undefined = undefined; - private _onLinuxMouseSelection = this.register(new EventEmitter()); - public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } - private _onRedrawRequest = this.register(new EventEmitter()); - public get onRequestRedraw(): IEvent { return this._onRedrawRequest.event; } - private _onSelectionChange = this.register(new EventEmitter()); - public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } - private _onRequestScrollLines = this.register(new EventEmitter()); - public get onRequestScrollLines(): IEvent { return this._onRequestScrollLines.event; } + public readonly onLinuxMouseSelection = this.register(initEvent()); + public readonly onRequestRedraw = this.register(initEvent()); + public readonly onSelectionChange = this.register(initEvent()); + public readonly onRequestScrollLines = this.register(initEvent()); constructor( private readonly _element: HTMLElement, @@ -260,7 +256,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._model.clearSelection(); this._removeMouseDownListeners(); this.refresh(); - this._onSelectionChange.fire(); + this.onSelectionChange.fire(); } /** @@ -279,7 +275,7 @@ export class SelectionService extends Disposable implements ISelectionService { if (Browser.isLinux && isLinuxMouseSelection) { const selectionText = this.selectionText; if (selectionText.length) { - this._onLinuxMouseSelection.fire(this.selectionText); + this.onLinuxMouseSelection.fire(this.selectionText); } } } @@ -290,7 +286,7 @@ export class SelectionService extends Disposable implements ISelectionService { */ private _refresh(): void { this._refreshAnimationFrame = undefined; - this._onRedrawRequest.fire({ + this.onRequestRedraw.fire({ start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd, columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN @@ -358,7 +354,7 @@ export class SelectionService extends Disposable implements ISelectionService { public selectAll(): void { this._model.isSelectAllActive = true; this.refresh(); - this._onSelectionChange.fire(); + this.onSelectionChange.fire(); } public selectLines(start: number, end: number): void { @@ -368,7 +364,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._model.selectionStart = [0, start]; this._model.selectionEnd = [this._bufferService.cols, end]; this.refresh(); - this._onSelectionChange.fire(); + this.onSelectionChange.fire(); } /** @@ -665,7 +661,7 @@ export class SelectionService extends Disposable implements ISelectionService { return; } if (this._dragScrollAmount) { - this._onRequestScrollLines.fire({ amount: this._dragScrollAmount, suppressScrollEvent: false }); + this.onRequestScrollLines.fire({ amount: this._dragScrollAmount, suppressScrollEvent: false }); // Re-evaluate selection // If the cursor was above or below the viewport, make sure it's at the // start or end of the viewport respectively. This should only happen when @@ -743,7 +739,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._oldSelectionStart = start; this._oldSelectionEnd = end; this._oldHasSelection = hasSelection; - this._onSelectionChange.fire(); + this.onSelectionChange.fire(); } private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void { diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 4d2c04ec..53743449 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -4,7 +4,7 @@ */ import { ICircularList } from 'common/Types'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { initEvent } from 'common/EventEmitter'; export interface IInsertEvent { index: number; @@ -25,12 +25,9 @@ export class CircularList implements ICircularList { private _startIndex: number; private _length: number; - public onDeleteEmitter = new EventEmitter(); - public get onDelete(): IEvent { return this.onDeleteEmitter.event; } - public onInsertEmitter = new EventEmitter(); - public get onInsert(): IEvent { return this.onInsertEmitter.event; } - public onTrimEmitter = new EventEmitter(); - public get onTrim(): IEvent { return this.onTrimEmitter.event; } + public readonly onDelete = initEvent(); + public readonly onInsert = initEvent(); + public readonly onTrim = initEvent(); constructor( private _maxLength: number @@ -107,7 +104,7 @@ export class CircularList implements ICircularList { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { this._startIndex = ++this._startIndex % this._maxLength; - this.onTrimEmitter.fire(1); + this.onTrim.fire(1); } else { this._length++; } @@ -123,7 +120,7 @@ export class CircularList implements ICircularList { throw new Error('Can only recycle when the buffer is full'); } this._startIndex = ++this._startIndex % this._maxLength; - this.onTrimEmitter.fire(1); + this.onTrim.fire(1); return this._array[this._getCyclicIndex(this._length - 1)]!; } @@ -158,7 +155,7 @@ export class CircularList implements ICircularList { this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)]; } this._length -= deleteCount; - this.onDeleteEmitter.fire({ index: start, amount: deleteCount }); + this.onDelete.fire({ index: start, amount: deleteCount }); } // Add items @@ -169,7 +166,7 @@ export class CircularList implements ICircularList { this._array[this._getCyclicIndex(start + i)] = items[i]; } if (items.length) { - this.onInsertEmitter.fire({ index: start, amount: items.length }); + this.onInsert.fire({ index: start, amount: items.length }); } // Adjust length as needed @@ -177,7 +174,7 @@ export class CircularList implements ICircularList { const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; this._length = this._maxLength; - this.onTrimEmitter.fire(countToTrim); + this.onTrim.fire(countToTrim); } else { this._length += items.length; } @@ -193,7 +190,7 @@ export class CircularList implements ICircularList { } this._startIndex += count; this._length -= count; - this.onTrimEmitter.fire(count); + this.onTrim.fire(count); } public shiftElements(start: number, count: number, offset: number): void { @@ -217,7 +214,7 @@ export class CircularList implements ICircularList { while (this._length > this._maxLength) { this._length--; this._startIndex++; - this.onTrimEmitter.fire(1); + this.onTrim.fire(1); } } } else { diff --git a/src/common/Color.ts b/src/common/Color.ts index b7ff177d..00d8613c 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -3,8 +3,14 @@ * @license MIT */ +import { isNode } from 'common/Platform'; import { IColor, IColorRGB } from 'common/Types'; +let $r = 0; +let $g = 0; +let $b = 0; +let $a = 0; + /** * Helper functions where the source type is "channels" (individual color channels as numbers). */ @@ -29,8 +35,8 @@ export namespace channels { */ export namespace color { export function blend(bg: IColor, fg: IColor): IColor { - const a = (fg.rgba & 0xFF) / 255; - if (a === 1) { + $a = (fg.rgba & 0xFF) / 255; + if ($a === 1) { return { css: fg.css, rgba: fg.rgba @@ -42,11 +48,11 @@ export namespace color { const bgR = (bg.rgba >> 24) & 0xFF; const bgG = (bg.rgba >> 16) & 0xFF; const bgB = (bg.rgba >> 8) & 0xFF; - const r = bgR + Math.round((fgR - bgR) * a); - const g = bgG + Math.round((fgG - bgG) * a); - const b = bgB + Math.round((fgB - bgB) * a); - const css = channels.toCss(r, g, b); - const rgba = channels.toRgba(r, g, b); + $r = bgR + Math.round((fgR - bgR) * $a); + $g = bgG + Math.round((fgG - bgG) * $a); + $b = bgB + Math.round((fgB - bgB) * $a); + const css = channels.toCss($r, $g, $b); + const rgba = channels.toRgba($r, $g, $b); return { css, rgba }; } @@ -68,25 +74,25 @@ export namespace color { export function opaque(color: IColor): IColor { const rgbaColor = (color.rgba | 0xFF) >>> 0; - const [r, g, b] = rgba.toChannels(rgbaColor); + [$r, $g, $b] = rgba.toChannels(rgbaColor); return { - css: channels.toCss(r, g, b), + css: channels.toCss($r, $g, $b), rgba: rgbaColor }; } export function opacity(color: IColor, opacity: number): IColor { - const a = Math.round(opacity * 0xFF); - const [r, g, b] = rgba.toChannels(color.rgba); + $a = Math.round(opacity * 0xFF); + [$r, $g, $b] = rgba.toChannels(color.rgba); return { - css: channels.toCss(r, g, b, a), - rgba: channels.toRgba(r, g, b, a) + css: channels.toCss($r, $g, $b, $a), + rgba: channels.toRgba($r, $g, $b, $a) }; } export function multiplyOpacity(color: IColor, factor: number): IColor { - const a = color.rgba & 0xFF; - return opacity(color, (a * factor) / 0xFF); + $a = color.rgba & 0xFF; + return opacity(color, ($a * factor) / 0xFF); } export function toColorRGB(color: IColor): IColorRGB { @@ -98,21 +104,45 @@ export namespace color { * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa'). */ export namespace css { + let $ctx: CanvasRenderingContext2D | undefined; + let $litmusColor: CanvasGradient | undefined; + if (!isNode) { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d', { + willReadFrequently: true + }); + if (ctx) { + $ctx = ctx; + $ctx.globalCompositeOperation = 'copy'; + $litmusColor = $ctx.createLinearGradient(0, 0, 1, 1); + } + } + + /** + * Converts a css string to an IColor, this should handle all valid CSS color strings and will + * throw if it's invalid. The ideal format to use is `#rrggbb[aa]` as it's the fastest to parse. + * + * Only `#rgb[a]`, `#rrggbb[aa]`, `rgb()` and `rgba()` formats are supported when run in a Node + * environment. + */ export function toColor(css: string): IColor { + // Formats: #rgb[a] and #rrggbb[aa] if (css.match(/#[\da-f]{3,8}/i)) { switch (css.length) { case 4: { // #rgb - const r = parseInt(css.slice(1, 2).repeat(2), 16); - const g = parseInt(css.slice(2, 3).repeat(2), 16); - const b = parseInt(css.slice(3, 4).repeat(2), 16); - return rgba.toColor(r, g, b); + $r = parseInt(css.slice(1, 2).repeat(2), 16); + $g = parseInt(css.slice(2, 3).repeat(2), 16); + $b = parseInt(css.slice(3, 4).repeat(2), 16); + return rgba.toColor($r, $g, $b); } case 5: { // #rgba - const r = parseInt(css.slice(1, 2).repeat(2), 16); - const g = parseInt(css.slice(2, 3).repeat(2), 16); - const b = parseInt(css.slice(3, 4).repeat(2), 16); - const a = parseInt(css.slice(4, 5).repeat(2), 16); - return rgba.toColor(r, g, b, a); + $r = parseInt(css.slice(1, 2).repeat(2), 16); + $g = parseInt(css.slice(2, 3).repeat(2), 16); + $b = parseInt(css.slice(3, 4).repeat(2), 16); + $a = parseInt(css.slice(4, 5).repeat(2), 16); + return rgba.toColor($r, $g, $b, $a); } case 7: // #rrggbb return { @@ -126,15 +156,45 @@ export namespace css { }; } } + + // Formats: rgb() or rgba() const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/); - if (rgbaMatch) { // rgb() or rgba() - const r = parseInt(rgbaMatch[1]); - const g = parseInt(rgbaMatch[2]); - const b = parseInt(rgbaMatch[3]); - const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); - return rgba.toColor(r, g, b, a); + if (rgbaMatch) { + $r = parseInt(rgbaMatch[1]); + $g = parseInt(rgbaMatch[2]); + $b = parseInt(rgbaMatch[3]); + $a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); + return rgba.toColor($r, $g, $b, $a); } - throw new Error('css.toColor: Unsupported css format'); + + // Validate the context is available for canvas-based color parsing + if (!$ctx || !$litmusColor) { + throw new Error('css.toColor: Unsupported css format'); + } + + // Validate the color using canvas fillStyle + // See https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles + $ctx.fillStyle = $litmusColor; + $ctx.fillStyle = css; + if (typeof $ctx.fillStyle !== 'string') { + throw new Error('css.toColor: Unsupported css format'); + } + + $ctx.fillRect(0, 0, 1, 1); + [$r, $g, $b, $a] = $ctx.getImageData(0, 0, 1, 1).data; + + // Validate the color is non-transparent as color hue gets lost when drawn to the canvas + if ($a !== 0xFF) { + throw new Error('css.toColor: Unsupported css format'); + } + + // Extract the color from the canvas' fillStyle property which exposes the color value in rgba() + // format + // See https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + return { + rgba: channels.toRgba($r, $g, $b, $a), + css + }; } } diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index d7cb0f7e..dd6953bb 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -22,16 +22,15 @@ */ import { Disposable } from 'common/Lifecycle'; -import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; import { InstantiationService } from 'common/services/InstantiationService'; import { LogService } from 'common/services/LogService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { OptionsService } from 'common/services/OptionsService'; import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource } from 'common/Types'; import { CoreService } from 'common/services/CoreService'; -import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, forwardEvent, initEvent } from 'common/EventEmitter'; import { CoreMouseService } from 'common/services/CoreMouseService'; -import { DirtyRowService } from 'common/services/DirtyRowService'; import { UnicodeService } from 'common/services/UnicodeService'; import { CharsetService } from 'common/services/CharsetService'; import { updateWindowsModeWrappedState } from 'common/WindowsMode'; @@ -49,7 +48,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected readonly _bufferService: IBufferService; protected readonly _logService: ILogService; protected readonly _charsetService: ICharsetService; - protected readonly _dirtyRowService: IDirtyRowService; protected readonly _oscLinkService: IOscLinkService; public readonly coreMouseService: ICoreMouseService; @@ -61,22 +59,18 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - private _onBinary = new EventEmitter(); - public get onBinary(): IEvent { return this._onBinary.event; } - private _onData = new EventEmitter(); - public get onData(): IEvent { return this._onData.event; } - protected _onLineFeed = new EventEmitter(); - public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onResize = new EventEmitter<{ cols: number, rows: number }>(); - public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - protected _onScroll = new EventEmitter(); - public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } - protected _onWriteParsed = new EventEmitter(); + public readonly onBinary = initEvent(); + public readonly onData = initEvent(); + public readonly onLineFeed = initEvent(); + public readonly onResize = initEvent<{ cols: number, rows: number }>(); + public readonly onWriteParsed = initEvent(); + /** * Internally we track the source of the scroll but this is meaningless outside the library so * it's filtered out. */ protected _onScrollApi?: EventEmitter; + protected _onScroll = new EventEmitter(); public get onScroll(): IEvent { if (!this._onScrollApi) { this._onScrollApi = new EventEmitter(); @@ -114,8 +108,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(ICoreService, this.coreService); this.coreMouseService = this._instantiationService.createInstance(CoreMouseService); this._instantiationService.setService(ICoreMouseService, this.coreMouseService); - this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); - this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this.unicodeService = this._instantiationService.createInstance(UnicodeService); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); @@ -124,27 +116,28 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(IOscLinkService, this._oscLinkService); // Register input handler and handle/forward events - this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService); - this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); + this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService); + this.register(forwardEvent(this._inputHandler.onLineFeed, this.onLineFeed)); this.register(this._inputHandler); // Setup listeners - this.register(forwardEvent(this._bufferService.onResize, this._onResize)); - this.register(forwardEvent(this.coreService.onData, this._onData)); - this.register(forwardEvent(this.coreService.onBinary, this._onBinary)); + this.register(forwardEvent(this._bufferService.onResize, this.onResize)); + this.register(forwardEvent(this.coreService.onData, this.onData)); + this.register(forwardEvent(this.coreService.onBinary, this.onBinary)); + this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput())); this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); this.register(this._bufferService.onScroll(event => { this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); - this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); this.register(this._inputHandler.onScroll(event => { this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); - this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); // Setup WriteBuffer this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); - this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed)); + this.register(forwardEvent(this._writeBuffer.onWriteParsed, this.onWriteParsed)); } public dispose(): void { diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 4684809f..29e931d2 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -14,13 +14,15 @@ export interface IEvent { } export interface IEventEmitter { - event: IEvent; fire(arg1: T, arg2: U): void; dispose(): void; } +export interface IEventWithEmitter extends IEventEmitter, IEvent { +} + export class EventEmitter implements IEventEmitter { - private _listeners: IListener[] = []; + private readonly _listeners: IListener[] = []; private _event?: IEvent; private _disposed: boolean = false; @@ -64,6 +66,34 @@ export class EventEmitter implements IEventEmitter { } } +/** + * Creates an object that implements both the {@link IEvent} and {@link IEmitter} interfaces. This + * allows more concise instantiation. The idea is to internally use the combined + * {@link IEventWithEmitter} interface and only expose {@link IEvent} externally. + * + * @example + * ```ts + * public readonly onFoo = initEvent(); + * // ... + * onFoo(e => handle(e)); + * onFoo.fire('bar'); + * ``` + */ +export function initEvent(): IEventWithEmitter { + const emitter = new EventEmitter(); + const event = emitter.event; + Object.defineProperty(event, '_listeners', { + value: (emitter as any)._listeners + }); + Object.defineProperty(event, 'fire', { + value: emitter.fire.bind(emitter) + }); + Object.defineProperty(event, 'dispose', { + value: emitter.dispose.bind(emitter) + }); + return event as any; +} + export function forwardEvent(from: IEvent, to: IEventEmitter): IDisposable { return from(e => to.fire(e)); } diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index f734b002..d9127e7b 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; import { IBufferService, ICoreService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -67,7 +67,7 @@ describe('InputHandler', () => { bufferService.resize(80, 30); coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); describe('SL/SR/DECIC/DECDC', () => { @@ -236,7 +236,7 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle bracketedPasteMode', () => { const coreService = new MockCoreService(); - const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); @@ -258,7 +258,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -305,7 +304,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -356,7 +354,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -394,7 +391,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -445,7 +441,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -572,7 +567,6 @@ describe('InputHandler', () => { new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -599,7 +593,7 @@ describe('InputHandler', () => { beforeEach(() => { bufferService = new MockBufferService(80, 30); - handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => { await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); @@ -796,7 +790,7 @@ describe('InputHandler', () => { describe('colon notation', () => { let inputHandler2: TestInputHandler; beforeEach(() => { - inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); describe('should equal to semicolon', () => { it('CSI 38:2::50:100:150 m', async () => { @@ -2278,7 +2272,7 @@ describe('InputHandler - async handlers', () => { coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); coreService.onData(data => { console.log(data); }); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); it('async CUP with CPR check', async () => { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b599bb7e..d4711c1f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -11,12 +11,12 @@ import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; +import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; import { IBuffer } from 'common/buffer/Types'; @@ -104,6 +104,8 @@ export enum WindowsOptionsReportType { // create a warning log if an async handler takes longer than the limit (in ms) const SLOW_ASYNC_LIMIT = 5000; +// Work variables to avoid garbage collection +let $temp = 0; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -120,6 +122,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _windowTitle = ''; private _iconName = ''; private _currentLinkId?: number; + private _dirtyRowTracker: IDirtyRowTracker; protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; @@ -129,33 +132,20 @@ export class InputHandler extends Disposable implements IInputHandler { private _activeBuffer: IBuffer; - private _onRequestBell = new EventEmitter(); - public get onRequestBell(): IEvent { return this._onRequestBell.event; } - private _onRequestRefreshRows = new EventEmitter(); - public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } - private _onRequestReset = new EventEmitter(); - public get onRequestReset(): IEvent { return this._onRequestReset.event; } - private _onRequestSendFocus = new EventEmitter(); - public get onRequestSendFocus(): IEvent { return this._onRequestSendFocus.event; } - private _onRequestSyncScrollBar = new EventEmitter(); - public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; } - private _onRequestWindowsOptionsReport = new EventEmitter(); - public get onRequestWindowsOptionsReport(): IEvent { return this._onRequestWindowsOptionsReport.event; } + public readonly onRequestBell = initEvent(); + public readonly onRequestRefreshRows = initEvent(); + public readonly onRequestReset = initEvent(); + public readonly onRequestSendFocus = initEvent(); + public readonly onRequestSyncScrollBar = initEvent(); + public readonly onRequestWindowsOptionsReport = initEvent(); - private _onA11yChar = new EventEmitter(); - public get onA11yChar(): IEvent { return this._onA11yChar.event; } - private _onA11yTab = new EventEmitter(); - public get onA11yTab(): IEvent { return this._onA11yTab.event; } - private _onCursorMove = new EventEmitter(); - public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onLineFeed = new EventEmitter(); - public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onScroll = new EventEmitter(); - public get onScroll(): IEvent { return this._onScroll.event; } - private _onTitleChange = new EventEmitter(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - private _onColor = new EventEmitter(); - public get onColor(): IEvent { return this._onColor.event; } + public readonly onA11yChar = initEvent(); + public readonly onA11yTab = initEvent(); + public readonly onCursorMove = initEvent(); + public readonly onLineFeed = initEvent(); + public readonly onScroll = initEvent(); + public readonly onTitleChange = initEvent(); + public readonly onColor = initEvent(); private _parseStack: IParseStack = { paused: false, @@ -169,7 +159,6 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _bufferService: IBufferService, private readonly _charsetService: ICharsetService, private readonly _coreService: ICoreService, - private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, private readonly _optionsService: IOptionsService, private readonly _oscLinkService: IOscLinkService, @@ -179,6 +168,7 @@ export class InputHandler extends Disposable implements IInputHandler { ) { super(); this.register(this._parser); + this._dirtyRowTracker = new DirtyRowTracker(this._bufferService); // Track properties used in performance critical code manually to avoid using slow getters this._activeBuffer = this._bufferService.buffer; @@ -459,7 +449,7 @@ export class InputHandler extends Disposable implements IInputHandler { // Clear the dirty row service so we know which lines changed as a result of parsing // Important: do not clear between async calls, otherwise we lost pending update information. if (!wasPaused) { - this._dirtyRowService.clearRange(); + this._dirtyRowTracker.clearRange(); } // process big data in smaller chunks @@ -489,11 +479,11 @@ export class InputHandler extends Disposable implements IInputHandler { } if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) { - this._onCursorMove.fire(); + this.onCursorMove.fire(); } // Refresh any dirty rows accumulated as part of parsing - this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end); + this.onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end); } public print(data: Uint32Array, start: number, end: number): void { @@ -507,7 +497,7 @@ export class InputHandler extends Disposable implements IInputHandler { const curAttr = this._curAttrData; let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { @@ -532,7 +522,7 @@ export class InputHandler extends Disposable implements IInputHandler { } if (screenReaderMode) { - this._onA11yChar.fire(stringFromCodePoint(code)); + this.onA11yChar.fire(stringFromCodePoint(code)); } if (this._currentLinkId !== undefined) { this._oscLinkService.addLineToLink(this._currentLinkId, this._activeBuffer.ybase + this._activeBuffer.y); @@ -635,7 +625,7 @@ export class InputHandler extends Disposable implements IInputHandler { bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } /** @@ -684,7 +674,7 @@ export class InputHandler extends Disposable implements IInputHandler { * and `ITerminalOptions.bellSound`. */ public bell(): boolean { - this._onRequestBell.fire(); + this.onRequestBell.fire(); return true; } @@ -699,7 +689,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF." */ public lineFeed(): boolean { - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); if (this._optionsService.rawOptions.convertEol) { this._activeBuffer.x = 0; } @@ -714,9 +704,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._activeBuffer.x >= this._bufferService.cols) { this._activeBuffer.x--; } - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); - this._onLineFeed.fire(); + this.onLineFeed.fire(); return true; } @@ -805,7 +795,7 @@ export class InputHandler extends Disposable implements IInputHandler { const originalX = this._activeBuffer.x; this._activeBuffer.x = this._activeBuffer.nextStop(); if (this._optionsService.rawOptions.screenReaderMode) { - this._onA11yTab.fire(this._activeBuffer.x - originalX); + this.onA11yTab.fire(this._activeBuffer.x - originalX); } return true; } @@ -842,14 +832,14 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.y = this._coreService.decPrivateModes.origin ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y)) : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y)); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } /** * Set absolute cursor position. */ private _setCursor(x: number, y: number): void { - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); if (this._coreService.decPrivateModes.origin) { this._activeBuffer.x = x; this._activeBuffer.y = this._activeBuffer.scrollTop + y; @@ -858,7 +848,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.y = y; } this._restrictCursor(); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } /** @@ -1178,16 +1168,16 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params.params[0]) { case 0: j = this._activeBuffer.y; - this._dirtyRowService.markDirty(j); + this._dirtyRowTracker.markDirty(j); this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); for (; j < this._bufferService.rows; j++) { this._resetBufferLine(j, respectProtect); } - this._dirtyRowService.markDirty(j); + this._dirtyRowTracker.markDirty(j); break; case 1: j = this._activeBuffer.y; - this._dirtyRowService.markDirty(j); + this._dirtyRowTracker.markDirty(j); // Deleted front part of line and everything before. This line will no longer be wrapped. this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect); if (this._activeBuffer.x + 1 >= this._bufferService.cols) { @@ -1197,15 +1187,15 @@ export class InputHandler extends Disposable implements IInputHandler { while (j--) { this._resetBufferLine(j, respectProtect); } - this._dirtyRowService.markDirty(0); + this._dirtyRowTracker.markDirty(0); break; case 2: j = this._bufferService.rows; - this._dirtyRowService.markDirty(j - 1); + this._dirtyRowTracker.markDirty(j - 1); while (j--) { this._resetBufferLine(j, respectProtect); } - this._dirtyRowService.markDirty(0); + this._dirtyRowTracker.markDirty(0); break; case 3: // Clear scrollback (everything not in viewport) @@ -1215,7 +1205,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0); this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0); // Force a scroll event to refresh viewport - this._onScroll.fire(0); + this.onScroll.fire(0); } break; } @@ -1257,7 +1247,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect); break; } - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); return true; } @@ -1289,7 +1279,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? return true; } @@ -1323,7 +1313,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? return true; } @@ -1349,7 +1339,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } return true; } @@ -1375,7 +1365,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } return true; } @@ -1395,7 +1385,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1); this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1411,7 +1401,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1); this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA)); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1443,7 +1433,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1476,7 +1466,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1499,7 +1489,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1522,7 +1512,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1544,7 +1534,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } return true; } @@ -1846,7 +1836,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ if (this._optionsService.rawOptions.windowOptions.setWinLines) { this._bufferService.resize(132, this._bufferService.rows); - this._onRequestReset.fire(); + this.onRequestReset.fire(); } break; case 6: @@ -1865,7 +1855,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; - this._onRequestSyncScrollBar.fire(); + this.onRequestSyncScrollBar.fire(); break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. @@ -1887,7 +1877,7 @@ export class InputHandler extends Disposable implements IInputHandler { // focusin: ^[[I // focusout: ^[[O this._coreService.decPrivateModes.sendFocus = true; - this._onRequestSendFocus.fire(); + this.onRequestSendFocus.fire(); break; case 1005: // utf8 ext mode mouse - removed in #2507 this._logService.debug('DECSET 1005 not supported (see #2507)'); @@ -1914,8 +1904,8 @@ export class InputHandler extends Disposable implements IInputHandler { case 1047: // alt screen buffer this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()); this._coreService.isCursorInitialized = true; - this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); - this._onRequestSyncScrollBar.fire(); + this.onRequestRefreshRows.fire(0, this._bufferService.rows - 1); + this.onRequestSyncScrollBar.fire(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) this._coreService.decPrivateModes.bracketedPasteMode = true; @@ -2084,7 +2074,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ if (this._optionsService.rawOptions.windowOptions.setWinLines) { this._bufferService.resize(80, this._bufferService.rows); - this._onRequestReset.fire(); + this.onRequestReset.fire(); } break; case 6: @@ -2103,7 +2093,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; - this._onRequestSyncScrollBar.fire(); + this.onRequestSyncScrollBar.fire(); break; case 9: // X10 Mouse case 1000: // vt200 mouse @@ -2142,8 +2132,8 @@ export class InputHandler extends Disposable implements IInputHandler { this.restoreCursor(); } this._coreService.isCursorInitialized = true; - this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); - this._onRequestSyncScrollBar.fire(); + this.onRequestRefreshRows.fire(0, this._bufferService.rows - 1); + this.onRequestSyncScrollBar.fire(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) this._coreService.decPrivateModes.bracketedPasteMode = false; @@ -2642,7 +2632,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public softReset(params: IParams): boolean { this._coreService.isCursorHidden = false; - this._onRequestSyncScrollBar.fire(); + this.onRequestSyncScrollBar.fire(); this._activeBuffer.scrollTop = 0; this._activeBuffer.scrollBottom = this._bufferService.rows - 1; this._curAttrData = DEFAULT_ATTR_DATA.clone(); @@ -2762,11 +2752,11 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params.params[0]) { case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t if (second !== 2) { - this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS); + this.onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS); } break; case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t - this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS); + this.onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS); break; case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t if (this._bufferService) { @@ -2856,7 +2846,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public setTitle(data: string): boolean { this._windowTitle = data; - this._onTitleChange.fire(data); + this.onTitleChange.fire(data); return true; } @@ -2898,7 +2888,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } if (event.length) { - this._onColor.fire(event); + this.onColor.fire(event); } return true; } @@ -2972,11 +2962,11 @@ export class InputHandler extends Disposable implements IInputHandler { for (let i = 0; i < slots.length; ++i, ++offset) { if (offset >= this._specialColors.length) break; if (slots[i] === '?') { - this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]); + this.onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]); } else { const color = parseColor(slots[i]); if (color) { - this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]); + this.onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]); } } } @@ -3037,7 +3027,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public restoreIndexedColor(data: string): boolean { if (!data) { - this._onColor.fire([{ type: ColorRequestType.RESTORE }]); + this.onColor.fire([{ type: ColorRequestType.RESTORE }]); return true; } const event: IColorEvent = []; @@ -3051,7 +3041,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } if (event.length) { - this._onColor.fire(event); + this.onColor.fire(event); } return true; } @@ -3062,7 +3052,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y OSC 110 "Restore default foreground color" "OSC 110 BEL" "Restore default foreground to themed color." */ public restoreFgColor(data: string): boolean { - this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]); + this.onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]); return true; } @@ -3072,7 +3062,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y OSC 111 "Restore default background color" "OSC 111 BEL" "Restore default background to themed color." */ public restoreBgColor(data: string): boolean { - this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]); + this.onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]); return true; } @@ -3082,7 +3072,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y OSC 112 "Restore default cursor color" "OSC 112 BEL" "Restore default cursor to themed color." */ public restoreCursorColor(data: string): boolean { - this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]); + this.onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]); return true; } @@ -3109,7 +3099,7 @@ export class InputHandler extends Disposable implements IInputHandler { public keypadApplicationMode(): boolean { this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; - this._onRequestSyncScrollBar.fire(); + this.onRequestSyncScrollBar.fire(); return true; } @@ -3121,7 +3111,7 @@ export class InputHandler extends Disposable implements IInputHandler { public keypadNumericMode(): boolean { this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; - this._onRequestSyncScrollBar.fire(); + this.onRequestSyncScrollBar.fire(); return true; } @@ -3220,7 +3210,7 @@ export class InputHandler extends Disposable implements IInputHandler { const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop; this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1); this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData())); - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); } else { this._activeBuffer.y--; this._restrictCursor(); // quickfix to not run out of bounds @@ -3235,7 +3225,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public fullReset(): boolean { this._parser.reset(); - this._onRequestReset.fire(); + this.onRequestReset.fire(); return true; } @@ -3293,7 +3283,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.isWrapped = false; } } - this._dirtyRowService.markAllDirty(); + this._dirtyRowTracker.markAllDirty(); this._setCursor(0, 0); return true; } @@ -3344,4 +3334,60 @@ export class InputHandler extends Disposable implements IInputHandler { if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`); return f(`P0$r`); } + + public markRangeDirty(y1: number, y2: number): void { + this._dirtyRowTracker.markRangeDirty(y1, y2); + } +} + +export interface IDirtyRowTracker { + readonly start: number; + readonly end: number; + + clearRange(): void; + markDirty(y: number): void; + markRangeDirty(y1: number, y2: number): void; + markAllDirty(): void; +} + +class DirtyRowTracker implements IDirtyRowTracker { + public start!: number; + public end!: number; + + constructor( + @IBufferService private readonly _bufferService: IBufferService + ) { + this.clearRange(); + } + + public clearRange(): void { + this.start = this._bufferService.buffer.y; + this.end = this._bufferService.buffer.y; + } + + public markDirty(y: number): void { + if (y < this.start) { + this.start = y; + } else if (y > this.end) { + this.end = y; + } + } + + public markRangeDirty(y1: number, y2: number): void { + if (y1 > y2) { + $temp = y1; + y1 = y2; + y2 = $temp; + } + if (y1 < this.start) { + this.start = y1; + } + if (y2 > this.end) { + this.end = y2; + } + } + + public markAllDirty(): void { + this.markRangeDirty(0, this._bufferService.rows - 1); + } } diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 7b823b12..6be0584f 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -13,7 +13,7 @@ interface INavigator { // we want this module to live in common. declare const navigator: INavigator; -const isNode = (typeof navigator === 'undefined') ? true : false; +export const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; diff --git a/src/common/TaskQueue.ts b/src/common/TaskQueue.ts new file mode 100644 index 00000000..94c5c53b --- /dev/null +++ b/src/common/TaskQueue.ts @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { isNode } from 'common/Platform'; + +interface ITaskQueue { + /** + * Adds a task to the queue which will run in a future idle callback. + */ + enqueue(task: () => void): void; + + /** + * Flushes the queue, running all remaining tasks synchronously. + */ + flush(): void; + + /** + * Clears any remaining tasks from the queue, these will not be run. + */ + clear(): void; +} + +interface ITaskDeadline { + timeRemaining(): number; +} +type CallbackWithDeadline = (deadline: ITaskDeadline) => void; + +abstract class TaskQueue implements ITaskQueue { + private _tasks: (() => void)[] = []; + private _idleCallback?: number; + private _i = 0; + + protected abstract _requestCallback(callback: CallbackWithDeadline): number; + protected abstract _cancelCallback(identifier: number): void; + + public enqueue(task: () => void): void { + this._tasks.push(task); + this._start(); + } + + public flush(): void { + while (this._i < this._tasks.length) { + this._tasks[this._i++](); + } + this.clear(); + } + + public clear(): void { + if (this._idleCallback) { + this._cancelCallback(this._idleCallback); + this._idleCallback = undefined; + } + this._i = 0; + this._tasks.length = 0; + } + + private _start(): void { + if (!this._idleCallback) { + this._idleCallback = this._requestCallback(this._process.bind(this)); + } + } + + private _process(deadline: ITaskDeadline): void { + this._idleCallback = undefined; + let taskDuration = 0; + let longestTask = 0; + while (this._i < this._tasks.length) { + taskDuration = performance.now(); + this._tasks[this._i++](); + taskDuration = performance.now() - taskDuration; + longestTask = Math.max(taskDuration, longestTask); + // Guess the following task will take a similar time to the longest task in this batch, allow + // additional room to try avoid exceeding the deadline + if (longestTask * 1.5 > deadline.timeRemaining()) { + this._start(); + return; + } + } + this.clear(); + } +} + +/** + * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames + * per second. The tasks will run in the order they are enqueued, but they will run some time later, + * and care should be taken to ensure they're non-urgent and will not introduce race conditions. + */ +export class PriorityTaskQueue extends TaskQueue { + protected _requestCallback(callback: CallbackWithDeadline): number { + return setTimeout(() => callback(this._createDeadline(16))); + } + + protected _cancelCallback(identifier: number): void { + clearTimeout(identifier); + } + + private _createDeadline(duration: number): ITaskDeadline { + const end = performance.now() + duration; + return { + timeRemaining: () => Math.max(0, end - performance.now()) + }; + } +} + +class IdleTaskQueueInternal extends TaskQueue { + protected _requestCallback(callback: IdleRequestCallback): number { + return requestIdleCallback(callback); + } + + protected _cancelCallback(identifier: number): void { + cancelIdleCallback(identifier); + } +} + +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + * + * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? IdleTaskQueueInternal : PriorityTaskQueue; + +/** + * An object that tracks a single debounced task that will run on the next idle frame. When called + * multiple times, only the last set task will run. + */ +export class DebouncedIdleTask { + private _queue: ITaskQueue; + + constructor() { + this._queue = new IdleTaskQueue(); + } + + public set(task: () => void): void { + this._queue.clear(); + this._queue.enqueue(task); + } + + public flush(): void { + this._queue.flush(); + } +} diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 8fb71a5d..2ac70a5e 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services'; +import { IEvent, EventEmitter, initEvent } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; @@ -17,8 +17,8 @@ export class MockBufferService implements IBufferService { public serviceBrand: any; public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; - public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event; - public onScroll: IEvent = new EventEmitter().event; + public onResize: IEvent<{ cols: number, rows: number }> = initEvent<{ cols: number, rows: number }>(); + public onScroll: IEvent = initEvent(); public isUserScrolling: boolean = false; constructor( public cols: number, @@ -60,7 +60,7 @@ export class MockCoreMouseService implements ICoreMouseService { public addProtocol(name: string): void { } public reset(): void { } public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } - public onProtocolChange: IEvent = new EventEmitter().event; + public onProtocolChange: IEvent = initEvent(); public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } @@ -92,24 +92,14 @@ export class MockCoreService implements ICoreService { sendFocus: false, wraparound: true }; - public onData: IEvent = new EventEmitter().event; - public onUserInput: IEvent = new EventEmitter().event; - public onBinary: IEvent = new EventEmitter().event; + public onData: IEvent = initEvent(); + public onUserInput: IEvent = initEvent(); + public onBinary: IEvent = initEvent(); public reset(): void { } public triggerDataEvent(data: string, wasUserInput?: boolean): void { } public triggerBinaryEvent(data: string): void { } } -export class MockDirtyRowService implements IDirtyRowService { - public serviceBrand: any; - public start: number = 0; - public end: number = 0; - public clearRange(): void { } - public markDirty(y: number): void { } - public markRangeDirty(y1: number, y2: number): void { } - public markAllDirty(): void { } -} - export class MockLogService implements ILogService { public serviceBrand: any; public logLevel = LogLevelEnum.DEBUG; @@ -123,7 +113,7 @@ export class MockOptionsService implements IOptionsService { public serviceBrand: any; public readonly rawOptions: Required = clone(DEFAULT_OPTIONS); public options: Required = this.rawOptions; - public onOptionChange: IEvent = new EventEmitter().event; + public onOptionChange: IEvent = initEvent(); constructor(testOptions?: Partial) { if (testOptions) { for (const key of Object.keys(testOptions)) { @@ -159,7 +149,7 @@ export class MockUnicodeService implements IUnicodeService { } public versions: string[] = []; public activeVersion: string = ''; - public onChange: IEvent = new EventEmitter().event; + public onChange: IEvent = initEvent(); public wcwidth = (codepoint: number): number => this._provider.wcwidth(codepoint); public getStringCellWidth(s: string): number { throw new Error('Method not implemented.'); @@ -169,8 +159,8 @@ export class MockUnicodeService implements IUnicodeService { export class MockDecorationService implements IDecorationService { public serviceBrand: any; public get decorations(): IterableIterator { return [].values(); } - public onDecorationRegistered = new EventEmitter().event; - public onDecorationRemoved = new EventEmitter().event; + public onDecorationRegistered = initEvent(); + public onDecorationRemoved = initEvent(); public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; } public reset(): void { } public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index d44bb197..6e6e93e7 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -4,7 +4,7 @@ */ import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm'; -import { IEvent, IEventEmitter } from 'common/EventEmitter'; +import { IEvent, IEventEmitter, IEventWithEmitter } from 'common/EventEmitter'; import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; import { IParams } from 'common/parser/Types'; import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; @@ -71,12 +71,9 @@ export interface ICircularList { maxLength: number; isFull: boolean; - onDeleteEmitter: IEventEmitter; - onDelete: IEvent; - onInsertEmitter: IEventEmitter; - onInsert: IEvent; - onTrimEmitter: IEventEmitter; - onTrim: IEvent; + onDelete: IEventWithEmitter; + onInsert: IEventWithEmitter; + onTrim: IEventWithEmitter; get(index: number): T | undefined; set(index: number, value: T): void; diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index e5ea7f5e..03297ba6 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -1071,7 +1071,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); const marker = buffer.addMarker(buffer.lines.length - 1); assert.equal(marker.line, buffer.lines.length - 1); - buffer.lines.onTrimEmitter.fire(1); + buffer.lines.onTrim.fire(1); assert.equal(marker.line, buffer.lines.length - 2); }); it('should dispose of a marker if it is trimmed off the buffer', () => { @@ -1081,7 +1081,7 @@ describe('Buffer', () => { const marker = buffer.addMarker(0); assert.equal(marker.isDisposed, false); assert.equal(buffer.markers.length, 1); - buffer.lines.onTrimEmitter.fire(1); + buffer.lines.onTrim.fire(1); assert.equal(marker.isDisposed, true); assert.equal(buffer.markers.length, 0); }); @@ -1094,7 +1094,7 @@ describe('Buffer', () => { marker.onDispose(() => eventStack.push('disposed')); assert.equal(marker.isDisposed, false); assert.equal(buffer.markers.length, 1); - buffer.lines.onTrimEmitter.fire(1); + buffer.lines.onTrim.fire(1); assert.equal(marker.isDisposed, true); assert.equal(buffer.markers.length, 0); assert.deepEqual(eventStack, ['disposed']); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index c8b0d1b2..ec3b9fcd 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -463,12 +463,12 @@ export class Buffer implements IBuffer { let insertCountEmitted = 0; for (let i = insertEvents.length - 1; i >= 0; i--) { insertEvents[i].index += insertCountEmitted; - this.lines.onInsertEmitter.fire(insertEvents[i]); + this.lines.onInsert.fire(insertEvents[i]); insertCountEmitted += insertEvents[i].amount; } const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); if (amountToTrim > 0) { - this.lines.onTrimEmitter.fire(amountToTrim); + this.lines.onTrim.fire(amountToTrim); } } } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 43e89839..875ac6c9 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -37,10 +37,8 @@ const enum Cell { export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); -/** Work variables to avoid garbage collection. */ -const w: { startIndex: number } = { - startIndex: 0 -}; +// Work variables to avoid garbage collection +let $startIndex = 0; /** * Typed array based bufferline implementation. @@ -178,10 +176,10 @@ export class BufferLine implements IBufferLine { * to GC as it significantly reduced the amount of new objects/references needed. */ public loadCell(index: number, cell: ICellData): ICellData { - w.startIndex = index * CELL_SIZE; - cell.content = this._data[w.startIndex + Cell.CONTENT]; - cell.fg = this._data[w.startIndex + Cell.FG]; - cell.bg = this._data[w.startIndex + Cell.BG]; + $startIndex = index * CELL_SIZE; + cell.content = this._data[$startIndex + Cell.CONTENT]; + cell.fg = this._data[$startIndex + Cell.FG]; + cell.bg = this._data[$startIndex + Cell.BG]; if (cell.content & Content.IS_COMBINED_MASK) { cell.combinedData = this._combined[index]; } diff --git a/src/common/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts index ece9a96e..e496cbbb 100644 --- a/src/common/buffer/BufferReflow.ts +++ b/src/common/buffer/BufferReflow.ts @@ -118,7 +118,7 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to const countToRemove = toRemove[++nextToRemoveIndex]; // Tell markers that there was a deletion - lines.onDeleteEmitter.fire({ + lines.onDelete.fire({ index: i - countRemovedSoFar, amount: countToRemove }); diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index f940bb8f..1fa6fc27 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -6,7 +6,7 @@ import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IAttributeData } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { Disposable } from 'common/Lifecycle'; @@ -19,8 +19,7 @@ export class BufferSet extends Disposable implements IBufferSet { private _alt!: Buffer; private _activeBuffer!: Buffer; - private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>()); - public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; } + public readonly onBufferActivate = this.register(initEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>()); /** * Create a new BufferSet for the given terminal. @@ -42,7 +41,7 @@ export class BufferSet extends Disposable implements IBufferSet { // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer this._alt = new Buffer(false, this._optionsService, this._bufferService); this._activeBuffer = this._normal; - this._onBufferActivate.fire({ + this.onBufferActivate.fire({ activeBuffer: this._normal, inactiveBuffer: this._alt }); @@ -86,7 +85,7 @@ export class BufferSet extends Disposable implements IBufferSet { this._alt.clearAllMarkers(); this._alt.clear(); this._activeBuffer = this._normal; - this._onBufferActivate.fire({ + this.onBufferActivate.fire({ activeBuffer: this._normal, inactiveBuffer: this._alt }); @@ -105,7 +104,7 @@ export class BufferSet extends Disposable implements IBufferSet { this._alt.x = this._normal.x; this._alt.y = this._normal.y; this._activeBuffer = this._alt; - this._onBufferActivate.fire({ + this.onBufferActivate.fire({ activeBuffer: this._alt, inactiveBuffer: this._normal }); diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index 72c4085c..958bd9ed 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { initEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IMarker } from 'common/Types'; @@ -15,8 +15,7 @@ export class Marker extends Disposable implements IMarker { public get id(): number { return this._id; } - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } + public readonly onDispose = initEvent(); constructor( public line: number @@ -31,7 +30,7 @@ export class Marker extends Disposable implements IMarker { this.isDisposed = true; this.line = -1; // Emit before super.dispose such that dispose listeners get a change to react - this._onDispose.fire(); + this.onDispose.fire(); super.dispose(); } } diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index 67fd751e..4a9c8555 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { initEvent, EventEmitter, IEvent } from 'common/EventEmitter'; declare const setTimeout: (handler: () => void, timeout?: number) => void; @@ -33,12 +33,6 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -// queueMicrotask polyfill for nodejs < v11 -const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined') - ? (cb: () => void) => { Promise.resolve().then(cb); } - : queueMicrotask; - - export class WriteBuffer { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; @@ -46,11 +40,16 @@ export class WriteBuffer { private _bufferOffset = 0; private _isSyncWriting = false; private _syncCalls = 0; - public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } - private _onWriteParsed = new EventEmitter(); + private _didUserInput = false; + + public readonly onWriteParsed = initEvent(); constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } + public handleUserInput(): void { + this._didUserInput = true; + } + /** * @deprecated Unreliable, to be removed soon. */ @@ -105,6 +104,19 @@ export class WriteBuffer { // schedule chunk processing for next event loop run if (!this._writeBuffer.length) { this._bufferOffset = 0; + + // If this is the first write call after the user has done some input, + // parse it immediately to minimize input latency, + // otherwise schedule for the next event + if (this._didUserInput) { + this._didUserInput = false; + this._pendingData += data.length; + this._writeBuffer.push(data); + this._callbacks.push(callback); + this._innerWrite(); + return; + } + setTimeout(() => this._innerWrite()); } @@ -194,7 +206,7 @@ export class WriteBuffer { // 2. spawn a promise immediately resolving to `true` // (executed on the same queue, thus properly aligned before continuation happens) result.catch(err => { - qmt(() => {throw err;}); + queueMicrotask(() => {throw err;}); return Promise.resolve(false); }).then(continuation); return; @@ -224,6 +236,6 @@ export class WriteBuffer { this._pendingData = 0; this._bufferOffset = 0; } - this._onWriteParsed.fire(); + this.onWriteParsed.fire(); } } diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts index d86f6bf5..a00962ac 100644 --- a/src/common/public/BufferNamespaceApi.ts +++ b/src/common/public/BufferNamespaceApi.ts @@ -5,19 +5,19 @@ import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm'; import { BufferApiView } from 'common/public/BufferApiView'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { IEvent, EventEmitter, initEvent } from 'common/EventEmitter'; import { ICoreTerminal } from 'common/Types'; export class BufferNamespaceApi implements IBufferNamespaceApi { private _normal: BufferApiView; private _alternate: BufferApiView; - private _onBufferChange = new EventEmitter(); - public get onBufferChange(): IEvent { return this._onBufferChange.event; } + + public readonly onBufferChange = initEvent(); constructor(private _core: ICoreTerminal) { this._normal = new BufferApiView(this._core.buffers.normal, 'normal'); this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate'); - this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)); + this._core.buffers.onBufferActivate(() => this.onBufferChange.fire(this.active)); } public get active(): IBufferApi { if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; } diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index e3b7dcd8..3614fcfa 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -6,7 +6,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services'; import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; @@ -22,10 +22,8 @@ export class BufferService extends Disposable implements IBufferService { /** Whether the user is scrolling (locks the scroll position) */ public isUserScrolling: boolean = false; - private _onResize = new EventEmitter<{ cols: number, rows: number }>(); - public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - private _onScroll = new EventEmitter(); - public get onScroll(): IEvent { return this._onScroll.event; } + public readonly onResize = initEvent<{ cols: number, rows: number }>(); + public readonly onScroll = initEvent(); public get buffer(): IBuffer { return this.buffers.active; } @@ -49,7 +47,7 @@ export class BufferService extends Disposable implements IBufferService { this.rows = rows; this.buffers.resize(cols, rows); this.buffers.setupTabStops(this.cols); - this._onResize.fire({ cols, rows }); + this.onResize.fire({ cols, rows }); } public reset(): void { @@ -118,7 +116,7 @@ export class BufferService extends Disposable implements IBufferService { buffer.ydisp = buffer.ybase; } - this._onScroll.fire(buffer.ydisp); + this.onScroll.fire(buffer.ydisp); } /** @@ -148,7 +146,7 @@ export class BufferService extends Disposable implements IBufferService { } if (!suppressScrollEvent) { - this._onScroll.fire(buffer.ydisp); + this.onScroll.fire(buffer.ydisp); } } diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 54e991f8..a5f1528a 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -3,7 +3,7 @@ * @license MIT */ import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; /** @@ -170,9 +170,10 @@ export class CoreMouseService implements ICoreMouseService { private _encodings: { [name: string]: CoreMouseEncoding } = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; - private _onProtocolChange = new EventEmitter(); private _lastEvent: ICoreMouseEvent | null = null; + public readonly onProtocolChange = initEvent(); + constructor( @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService @@ -205,7 +206,7 @@ export class CoreMouseService implements ICoreMouseService { throw new Error(`unknown protocol "${name}"`); } this._activeProtocol = name; - this._onProtocolChange.fire(this._protocols[name].events); + this.onProtocolChange.fire(this._protocols[name].events); } public get activeEncoding(): string { @@ -225,13 +226,6 @@ export class CoreMouseService implements ICoreMouseService { this._lastEvent = null; } - /** - * Event to announce changes in mouse tracking. - */ - public get onProtocolChange(): IEvent { - return this._onProtocolChange.event; - } - /** * Triggers a mouse event to be sent. * diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 20a34603..35c75919 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -4,7 +4,7 @@ */ import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { IDecPrivateModes, IModes } from 'common/Types'; import { clone } from 'common/Clone'; import { Disposable } from 'common/Lifecycle'; @@ -34,12 +34,9 @@ export class CoreService extends Disposable implements ICoreService { // Circular dependency, this must be unset or memory will leak after Terminal.dispose private _scrollToBottom: (() => void) | undefined; - private _onData = this.register(new EventEmitter()); - public get onData(): IEvent { return this._onData.event; } - private _onUserInput = this.register(new EventEmitter()); - public get onUserInput(): IEvent { return this._onUserInput.event; } - private _onBinary = this.register(new EventEmitter()); - public get onBinary(): IEvent { return this._onBinary.event; } + public readonly onData = this.register(initEvent()); + public readonly onUserInput = this.register(initEvent()); + public readonly onBinary = this.register(initEvent()); constructor( // TODO: Move this into a service @@ -74,12 +71,12 @@ export class CoreService extends Disposable implements ICoreService { // Fire onUserInput so listeners can react as well (eg. clear selection) if (wasUserInput) { - this._onUserInput.fire(); + this.onUserInput.fire(); } // Fire onData API this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); - this._onData.fire(data); + this.onData.fire(data); } public triggerBinaryEvent(data: string): void { @@ -87,6 +84,6 @@ export class CoreService extends Disposable implements ICoreService { return; } this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); - this._onBinary.fire(data); + this.onBinary.fire(data); } } diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index e5d115a1..5efeb77d 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -4,18 +4,16 @@ */ import { css } from 'common/Color'; -import { EventEmitter } from 'common/EventEmitter'; +import { EventEmitter, initEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; import { SortedList } from 'common/SortedList'; import { IColor } from 'common/Types'; import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; -/** Work variables to avoid garbage collection. */ -const w = { - xmin: 0, - xmax: 0 -}; +// Work variables to avoid garbage collection +let $xmin = 0; +let $xmax = 0; export class DecorationService extends Disposable implements IDecorationService { public serviceBrand: any; @@ -27,10 +25,8 @@ export class DecorationService extends Disposable implements IDecorationService */ private readonly _decorations: SortedList = new SortedList(e => e?.marker.line); - private _onDecorationRegistered = this.register(new EventEmitter()); - public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; } - private _onDecorationRemoved = this.register(new EventEmitter()); - public get onDecorationRemoved(): IEvent { return this._onDecorationRemoved.event; } + public readonly onDecorationRegistered = this.register(initEvent()); + public readonly onDecorationRemoved = this.register(initEvent()); public get decorations(): IterableIterator { return this._decorations.values(); } @@ -44,13 +40,13 @@ export class DecorationService extends Disposable implements IDecorationService decoration.onDispose(() => { if (decoration) { if (this._decorations.delete(decoration)) { - this._onDecorationRemoved.fire(decoration); + this.onDecorationRemoved.fire(decoration); } markerDispose.dispose(); } }); this._decorations.insert(decoration); - this._onDecorationRegistered.fire(decoration); + this.onDecorationRegistered.fire(decoration); } return decoration; } @@ -76,9 +72,9 @@ export class DecorationService extends Disposable implements IDecorationService public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { this._decorations.forEachByKey(line, d => { - w.xmin = d.options.x ?? 0; - w.xmax = w.xmin + (d.options.width ?? 1); - if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { + $xmin = d.options.x ?? 0; + $xmax = $xmin + (d.options.width ?? 1); + if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { callback(d); } }); @@ -86,7 +82,7 @@ export class DecorationService extends Disposable implements IDecorationService public dispose(): void { for (const d of this._decorations.values()) { - this._onDecorationRemoved.fire(d); + this.onDecorationRemoved.fire(d); } this.reset(); } @@ -97,10 +93,8 @@ class Decoration extends Disposable implements IInternalDecoration { public element: HTMLElement | undefined; public isDisposed: boolean = false; - public readonly onRenderEmitter = this.register(new EventEmitter()); - public readonly onRender = this.onRenderEmitter.event; - private _onDispose = this.register(new EventEmitter()); - public readonly onDispose = this._onDispose.event; + public readonly onRender = this.register(initEvent()); + public readonly onDispose = this.register(initEvent()); private _cachedBg: IColor | undefined | null = null; public get backgroundColorRGB(): IColor | undefined { @@ -141,7 +135,7 @@ class Decoration extends Disposable implements IInternalDecoration { return; } this._isDisposed = true; - this._onDispose.fire(); + this.onDispose.fire(); super.dispose(); } } diff --git a/src/common/services/DirtyRowService.ts b/src/common/services/DirtyRowService.ts deleted file mode 100644 index 1c43b67e..00000000 --- a/src/common/services/DirtyRowService.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IBufferService, IDirtyRowService } from 'common/services/Services'; - -export class DirtyRowService implements IDirtyRowService { - public serviceBrand: any; - - private _start!: number; - private _end!: number; - - public get start(): number { return this._start; } - public get end(): number { return this._end; } - - constructor( - @IBufferService private readonly _bufferService: IBufferService - ) { - this.clearRange(); - } - - public clearRange(): void { - this._start = this._bufferService.buffer.y; - this._end = this._bufferService.buffer.y; - } - - public markDirty(y: number): void { - if (y < this._start) { - this._start = y; - } else if (y > this._end) { - this._end = y; - } - } - - public markRangeDirty(y1: number, y2: number): void { - if (y1 > y2) { - const temp = y1; - y1 = y2; - y2 = temp; - } - if (y1 < this._start) { - this._start = y1; - } - if (y2 > this._end) { - this._end = y2; - } - } - - public markAllDirty(): void { - this.markRangeDirty(0, this._bufferService.rows - 1); - } -} diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index c7e8d294..16beddba 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -4,9 +4,9 @@ */ import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; import { isMac } from 'common/Platform'; import { CursorStyle } from 'common/Types'; +import { initEvent } from 'common/EventEmitter'; export const DEFAULT_OPTIONS: Readonly> = { cols: 80, @@ -57,8 +57,7 @@ export class OptionsService implements IOptionsService { public readonly rawOptions: Required; public options: Required; - private _onOptionChange = new EventEmitter(); - public get onOptionChange(): IEvent { return this._onOptionChange.event; } + public readonly onOptionChange = initEvent(); constructor(options: Partial) { // set the default value of each option @@ -97,7 +96,7 @@ export class OptionsService implements IOptionsService { // Don't fire an option change event if they didn't change if (this.rawOptions[propName] !== value) { this.rawOptions[propName] = value; - this._onOptionChange.fire(propName); + this.onOptionChange.fire(propName); } }; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 22edad1d..c47b1c2a 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IEvent, IEventEmitter } from 'common/EventEmitter'; +import { IEvent, IEventEmitter, IEventWithEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; @@ -122,19 +122,6 @@ export interface ICharsetService { setgCharset(g: number, charset: ICharset | undefined): void; } -export const IDirtyRowService = createDecorator('DirtyRowService'); -export interface IDirtyRowService { - serviceBrand: undefined; - - readonly start: number; - readonly end: number; - - clearRange(): void; - markDirty(y: number): void; - markRangeDirty(y1: number, y2: number): void; - markAllDirty(): void; -} - export interface IServiceIdentifier { (...args: any[]): void; type: T; @@ -331,5 +318,5 @@ export interface IInternalDecoration extends IDecoration { readonly options: IDecorationOptions; readonly backgroundColorRGB: IColor | undefined; readonly foregroundColorRGB: IColor | undefined; - readonly onRenderEmitter: IEventEmitter; + readonly onRender: IEventWithEmitter; } diff --git a/src/common/services/UnicodeService.ts b/src/common/services/UnicodeService.ts index e96b7579..7306db93 100644 --- a/src/common/services/UnicodeService.ts +++ b/src/common/services/UnicodeService.ts @@ -3,7 +3,7 @@ * @license MIT */ import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { UnicodeV6 } from 'common/input/UnicodeV6'; @@ -13,8 +13,8 @@ export class UnicodeService implements IUnicodeService { private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null); private _active: string = ''; private _activeProvider: IUnicodeVersionProvider; - private _onChange = new EventEmitter(); - public get onChange(): IEvent { return this._onChange.event; } + + public readonly onChange = initEvent(); constructor() { const defaultProvider = new UnicodeV6(); @@ -37,7 +37,7 @@ export class UnicodeService implements IUnicodeService { } this._active = version; this._activeProvider = this._providers[version]; - this._onChange.fire(version); + this.onChange.fire(version); } public register(provider: IUnicodeVersionProvider): void { diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 1cad0ee2..45b8aa74 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -24,7 +24,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBuffer } from 'common/buffer/Types'; import { CoreTerminal } from 'common/CoreTerminal'; -import { EventEmitter, forwardEvent, IEvent } from 'common/EventEmitter'; +import { EventEmitter, forwardEvent, IEvent, initEvent } from 'common/EventEmitter'; import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services'; import { IMarker, ITerminalOptions, ScrollSource } from 'common/Types'; @@ -32,17 +32,11 @@ export class Terminal extends CoreTerminal { // TODO: We should remove options once components adopt optionsService public get options(): Required { return this.optionsService.options; } - private _onBell = new EventEmitter(); - public get onBell(): IEvent { return this._onBell.event; } - private _onCursorMove = new EventEmitter(); - public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onTitleChange = new EventEmitter(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - - private _onA11yCharEmitter = new EventEmitter(); - public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } - private _onA11yTabEmitter = new EventEmitter(); - public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } + public readonly onBell = initEvent(); + public readonly onCursorMove = initEvent(); + public readonly onTitleChange = initEvent(); + public readonly onA11yChar = initEvent(); + public readonly onA11yTab = initEvent(); /** * Creates a new `Terminal` object. @@ -66,10 +60,10 @@ export class Terminal extends CoreTerminal { // Setup InputHandler listeners this.register(this._inputHandler.onRequestBell(() => this.bell())); this.register(this._inputHandler.onRequestReset(() => this.reset())); - this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); - this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); - this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); - this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + this.register(forwardEvent(this._inputHandler.onCursorMove, this.onCursorMove)); + this.register(forwardEvent(this._inputHandler.onTitleChange, this.onTitleChange)); + this.register(forwardEvent(this._inputHandler.onA11yChar, this.onA11yChar)); + this.register(forwardEvent(this._inputHandler.onA11yTab, this.onA11yTab)); } public dispose(): void { @@ -112,7 +106,7 @@ export class Terminal extends CoreTerminal { } public bell(): void { - this._onBell.fire(); + this.onBell.fire(); } /** diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 1c0a986a..7453b7e2 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -19,8 +19,7 @@ declare module 'xterm-headless' { export interface ITerminalOptions { /** * Whether to allow the use of proposed API. When false, any usage of APIs - * marked as experimental/proposed will throw an error. This defaults to - * true currently, but will change to false in v5.0. + * marked as experimental/proposed will throw an error. The default is false. */ allowProposedApi?: boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d66e3b03..0e2b0357 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -26,8 +26,7 @@ declare module 'xterm' { export interface ITerminalOptions { /** * Whether to allow the use of proposed API. When false, any usage of APIs - * marked as experimental/proposed will throw an error. This defaults to - * true currently, but will change to false in v5.0. + * marked as experimental/proposed will throw an error. The default is false. */ allowProposedApi?: boolean;