From 3d41f028e4966eda695a069b2ff1ddeab450a6b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 10:12:48 -0700 Subject: [PATCH 1/8] Layout dimension left/right after renderer dims change Fixes #3773 --- .../Decorations/BufferDecorationRenderer.ts | 23 ++++++++++++++----- src/browser/TestUtils.test.ts | 2 +- src/browser/services/RenderService.ts | 4 ++-- src/browser/services/Services.ts | 1 - 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts index 22dc73e9..5bd0f940 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -14,6 +14,7 @@ export class BufferDecorationRenderer extends Disposable { private _animationFrame: number | undefined; private _altBufferIsActive: boolean = false; + private _dimensionsChanged: boolean = false; constructor( private readonly _screenElement: HTMLElement, @@ -28,7 +29,10 @@ export class BufferDecorationRenderer extends Disposable { this._screenElement.appendChild(this._container); this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); - this.register(this._renderService.onDimensionsChange(() => this._queueRefresh())); + this.register(this._renderService.onDimensionsChange(() => { + this._dimensionsChanged = true; + this._queueRefresh(); + })); this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh())); this.register(this._bufferService.buffers.onBufferActivate(() => { this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt; @@ -70,6 +74,9 @@ export class BufferDecorationRenderer extends Disposable { this._container.appendChild(element); } this._refreshStyle(decoration, element); + if (this._dimensionsChanged) { + this._refreshXPosition(decoration, element); + } decoration.onRenderEmitter.fire(element); } @@ -86,11 +93,7 @@ export class BufferDecorationRenderer extends Disposable { // exceeded the container width, so hide element.style.display = 'none'; } - if ((decoration.options.anchor || 'left') === 'right') { - element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; - } else { - element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; - } + this._refreshXPosition(decoration, element, x); return element; } @@ -106,6 +109,14 @@ export class BufferDecorationRenderer extends Disposable { } } + private _refreshXPosition(decoration: IInternalDecoration, element: HTMLElement, x: number = decoration.options.x ?? 0): void { + if ((decoration.options.anchor || 'left') === 'right') { + element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + } else { + element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + } + } + private _removeDecoration(decoration: IInternalDecoration): void { this._decorationElements.get(decoration)?.remove(); this._decorationElements.delete(decoration); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 85e2bb55..cc45f5e4 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -396,7 +396,7 @@ export class MockRenderService implements IRenderService { public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } - public changeOptions(): void { + public _handleOptionsChanged(): void { throw new Error('Method not implemented.'); } public setRenderer(renderer: IRenderer): void { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 91b510a3..b575858a 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -69,7 +69,7 @@ export class RenderService extends Disposable implements IRenderService { this.register(bufferService.onResize(() => this._fullRefresh())); this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear())); - this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); // No need to register this as renderer is explicitly disposed in RenderService.dispose @@ -135,7 +135,7 @@ export class RenderService extends Disposable implements IRenderService { this._fireOnCanvasResize(); } - public changeOptions(): void { + private _handleOptionsChanged(): void { this._renderer.onOptionsChanged(); this.refreshRows(0, this._rowCount - 1); this._fireOnCanvasResize(); diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 7191d0ed..a9f76a90 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -61,7 +61,6 @@ export interface IRenderService extends IDisposable { refreshRows(start: number, end: number): void; clearTextureAtlas(): void; resize(cols: number, rows: number): void; - changeOptions(): void; setRenderer(renderer: IRenderer): void; setColors(colors: IColorSet): void; onDevicePixelRatioChange(): void; From aeb4fa0fe9da0d43fc10674aebd36ba4ada9433e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 10:15:18 -0700 Subject: [PATCH 2/8] Fix compile after bad merge --- .../Decorations/BufferDecorationRenderer.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts index f548750a..e695c824 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -66,7 +66,7 @@ export class BufferDecorationRenderer extends Disposable { private _renderDecoration(decoration: IInternalDecoration): void { this._refreshStyle(decoration); if (this._dimensionsChanged) { - this._refreshXPosition(decoration, element); + this._refreshXPosition(decoration); } } @@ -83,7 +83,7 @@ export class BufferDecorationRenderer extends Disposable { // exceeded the container width, so hide element.style.display = 'none'; } - this._refreshXPosition(decoration, element, x); + this._refreshXPosition(decoration); return element; } @@ -111,11 +111,15 @@ export class BufferDecorationRenderer extends Disposable { } } - private _refreshXPosition(decoration: IInternalDecoration, element: HTMLElement, x: number = decoration.options.x ?? 0): void { + private _refreshXPosition(decoration: IInternalDecoration): void { + if (!decoration.element) { + return; + } + const x = decoration.options.x ?? 0; if ((decoration.options.anchor || 'left') === 'right') { - element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + decoration.element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; } else { - element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + decoration.element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; } } From 0976b15709f7dd7649c4471a6e59349d4ace8787 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 11:57:13 -0700 Subject: [PATCH 3/8] Fix lint --- src/browser/TestUtils.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index cc45f5e4..cb487dbc 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -396,9 +396,6 @@ export class MockRenderService implements IRenderService { public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } - public _handleOptionsChanged(): void { - throw new Error('Method not implemented.'); - } public setRenderer(renderer: IRenderer): void { throw new Error('Method not implemented.'); } From f18528d2bb82dc45bd51c1a2c835290ef7f699dc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 10:58:50 -0700 Subject: [PATCH 4/8] Clear _dimensionsChanged variable --- src/browser/Decorations/BufferDecorationRenderer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts index e695c824..a063f9bd 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -61,6 +61,7 @@ export class BufferDecorationRenderer extends Disposable { for (const decoration of this._decorationService.decorations) { this._renderDecoration(decoration); } + this._dimensionsChanged = false; } private _renderDecoration(decoration: IInternalDecoration): void { From de03391c2d6bc1eb3b66c56e838ec8968b85cad8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 13 May 2022 11:19:38 -0700 Subject: [PATCH 5/8] Allow ensureContrastRatio to change luminance the other way The higher ratio of the two results will be picked. Fixes #3720 --- src/common/Color.ts | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/common/Color.ts b/src/common/Color.ts index b197cd66..a2a3cbad 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -150,15 +150,42 @@ export namespace rgb { * Helper functions where the source type is "rgba" (number: 0xrrggbbaa). */ export namespace rgba { + /** + * Given a foreground color and a background color, either increase or reduce the luminance of the + * foreground color until the specified contrast ratio is met. If pure white or black is hit + * without the contrast ratio being met, go the other direction using the background color as the + * foreground color and take either the first or second result depending on which has the higher + * contrast ratio. + * + * `undefined` will be returned if the contrast ratio is already met. + * + * @param bgRgba The background color in rgba format. + * @param fgRgba The foreground color in rgba format. + * @param ratio The contrast ratio to achieve. + */ export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined { const bgL = rgb.relativeLuminance(bgRgba >> 8); const fgL = rgb.relativeLuminance(fgRgba >> 8); const cr = contrastRatio(bgL, fgL); if (cr < ratio) { if (fgL < bgL) { - return reduceLuminance(bgRgba, fgRgba, ratio); + const resultA = reduceLuminance(bgRgba, fgRgba, ratio); + const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); + if (resultARatio < ratio) { + const resultB = increaseLuminance(bgRgba, bgRgba, ratio); + const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); + return resultARatio > resultBRatio ? resultA : resultB; + } + return resultA; } - return increaseLuminance(bgRgba, fgRgba, ratio); + const resultA = increaseLuminance(bgRgba, fgRgba, ratio); + const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); + if (resultARatio < ratio) { + const resultB = reduceLuminance(bgRgba, bgRgba, ratio); + const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); + return resultARatio > resultBRatio ? resultA : resultB; + } + return resultA; } return undefined; } From d7fb0141d246399f3f5ea375543f5a5733439fbb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 13 May 2022 12:32:17 -0700 Subject: [PATCH 6/8] Whitespace change to trigger build The fix for #3720 didn't touch any file in the webgl addon so it didn't end up releasing. --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index d060c4d1..4739d498 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -167,7 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._rectangleRenderer.onResize(); - this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); From de5df69333ef97e5d25ae65ec12659c1fd1754a0 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 17 May 2022 09:52:46 -0700 Subject: [PATCH 7/8] share animation frame with renderService (#3796) --- src/browser/RenderDebouncer.ts | 25 ++++++++++++++++--- src/browser/TestUtils.test.ts | 5 +++- src/browser/Types.d.ts | 4 +++ .../decorations/BufferDecorationRenderer.ts | 2 +- src/browser/services/RenderService.ts | 8 ++++-- src/browser/services/Services.ts | 4 ++- 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index 02521070..ad2d79b4 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { IRenderDebouncer } from 'browser/Types'; +import { IRenderDebouncerWithCallback } from 'browser/Types'; /** * Debounces calls to render terminal rows using animation frames. */ -export class RenderDebouncer implements IRenderDebouncer { +export class RenderDebouncer implements IRenderDebouncerWithCallback { private _rowStart: number | undefined; private _rowEnd: number | undefined; private _rowCount: number | undefined; private _animationFrame: number | undefined; + private _refreshCallbacks: FrameRequestCallback[] = []; constructor( private _renderCallback: (start: number, end: number) => void @@ -26,6 +27,14 @@ export class RenderDebouncer implements IRenderDebouncer { } } + public addRefreshCallback(callback: FrameRequestCallback): number { + this._refreshCallbacks.push(callback); + if (!this._animationFrame) { + this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + } + return this._animationFrame; + } + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { this._rowCount = rowCount; // Get the min/max row start/end for the arg values @@ -43,8 +52,11 @@ export class RenderDebouncer implements IRenderDebouncer { } private _innerRefresh(): void { + this._animationFrame = undefined; + // Make sure values are set if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + this._runRefreshCallbacks(); return; } @@ -55,9 +67,16 @@ export class RenderDebouncer implements IRenderDebouncer { // Reset debouncer (this happens before render callback as the render could trigger it again) this._rowStart = undefined; this._rowEnd = undefined; - this._animationFrame = undefined; // Run render callback this._renderCallback(start, end); + this._runRefreshCallbacks(); + } + + private _runRefreshCallbacks(): void { + for (const callback of this._refreshCallbacks) { + callback(0); + } + this._refreshCallbacks = []; } } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index cb487dbc..c3376b21 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -7,7 +7,7 @@ import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOpt import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types'; +import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; @@ -390,6 +390,9 @@ export class MockRenderService implements IRenderService { public refreshRows(start: number, end: number): void { throw new Error('Method not implemented.'); } + public addRefreshCallback(callback: FrameRequestCallback): number { + throw new Error('Method not implemented.'); + } public clearTextureAtlas(): void { throw new Error('Method not implemented.'); } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 0e83c213..a472326a 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -309,3 +309,7 @@ export interface ICharacterJoiner { export interface IRenderDebouncer extends IDisposable { refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void; } + +export interface IRenderDebouncerWithCallback extends IRenderDebouncer { + addRefreshCallback(callback: FrameRequestCallback): number; +} diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index 00b2b00b..5c9b1282 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -51,7 +51,7 @@ export class BufferDecorationRenderer extends Disposable { if (this._animationFrame !== undefined) { return; } - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._renderService.addRefreshCallback(() => { this.refreshDecorations(); this._animationFrame = undefined; }); diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 75c2d3c8..852d8dc4 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IRenderDebouncer } from 'browser/Types'; +import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; @@ -22,7 +22,7 @@ interface ISelectionState { export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; - private _renderDebouncer: IRenderDebouncer; + private _renderDebouncer: IRenderDebouncerWithCallback; private _screenDprMonitor: ScreenDprMonitor; private _isPaused: boolean = false; @@ -171,6 +171,10 @@ export class RenderService extends Disposable implements IRenderService { this._fullRefresh(); } + public addRefreshCallback(callback: FrameRequestCallback): number { + return this._renderDebouncer.addRefreshCallback(callback); + } + private _fullRefresh(): void { if (this._isPaused) { this._needsFullRefresh = true; diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index a9f76a90..056bf91c 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IRenderDebouncer } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDisposable } from 'common/Types'; @@ -58,6 +58,8 @@ export interface IRenderService extends IDisposable { dimensions: IRenderDimensions; + addRefreshCallback(callback: FrameRequestCallback): number; + refreshRows(start: number, end: number): void; clearTextureAtlas(): void; resize(cols: number, rows: number): void; From a5f2ea336eff69c784414f371baacde641f03517 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 17 May 2022 10:00:55 -0700 Subject: [PATCH 8/8] update search results `onBufferContentsChange` (#3811) --- addons/xterm-addon-search/src/SearchAddon.ts | 2 +- addons/xterm-addon-web-links/test/tsconfig.json | 3 +-- src/browser/Linkifier2.ts | 2 +- src/browser/Terminal.ts | 2 +- src/browser/TestUtils.test.ts | 3 ++- src/browser/Types.d.ts | 1 + src/browser/decorations/BufferDecorationRenderer.ts | 2 +- src/browser/decorations/OverviewRulerRenderer.ts | 2 +- src/browser/public/Terminal.ts | 1 + src/browser/services/RenderService.ts | 6 +++--- src/browser/services/Services.ts | 2 +- src/common/CoreTerminal.ts | 3 +++ src/common/input/WriteBuffer.ts | 5 +++++ typings/xterm.d.ts | 11 +++++++++++ 14 files changed, 33 insertions(+), 12 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 1eb126e5..0f356e14 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -77,7 +77,7 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._onDataDisposable = this._terminal.onData(() => this._updateMatches()); + this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches()); this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches()); } diff --git a/addons/xterm-addon-web-links/test/tsconfig.json b/addons/xterm-addon-web-links/test/tsconfig.json index 1c772984..9f4d23df 100644 --- a/addons/xterm-addon-web-links/test/tsconfig.json +++ b/addons/xterm-addon-web-links/test/tsconfig.json @@ -12,8 +12,7 @@ "strict": true, "types": [ "../../../node_modules/@types/mocha", - "../../../node_modules/@types/node", - "../../../out-test/api/TestUtils" + "../../../node_modules/@types/node" ] }, "include": [ diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 89542936..acbf1c93 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -303,7 +303,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { // Add listener for rerendering if (this._renderService) { - this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => { + this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => { // When start is 0 a scroll most likely occurred, make sure links above the fold also get // cleared. const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp; diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index de3fff90..9ab3087c 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -526,7 +526,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.onRenderedBufferChange(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'); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index c3376b21..573720fc 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -30,6 +30,7 @@ export class MockTerminal implements ITerminal { public onBlur!: IEvent; public onFocus!: IEvent; public onA11yChar!: IEvent; + public onWriteParsed!: IEvent; public onA11yTab!: IEvent; public onCursorMove!: IEvent; public onLineFeed!: IEvent; @@ -370,7 +371,7 @@ export class MockMouseService implements IMouseService { export class MockRenderService implements IRenderService { public serviceBrand: undefined; public onDimensionsChange: IEvent = new EventEmitter().event; - public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().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 dimensions: IRenderDimensions = { diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index a472326a..f8ca03df 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -44,6 +44,7 @@ export interface IPublicTerminal extends IDisposable { onSelectionChange: IEvent; onRender: IEvent<{ start: number, end: number }>; onResize: IEvent<{ cols: number, rows: number }>; + onWriteParsed: IEvent; onTitleChange: IEvent; onBell: IEvent; blur(): void; diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index 5c9b1282..632a2864 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -28,7 +28,7 @@ export class BufferDecorationRenderer extends Disposable { this._container.classList.add('xterm-decoration-container'); this._screenElement.appendChild(this._container); - this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); + this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh())); this.register(this._renderService.onDimensionsChange(() => { this._dimensionsChanged = true; this._queueRefresh(); diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index 39480ca2..f31409ca 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -82,7 +82,7 @@ export class OverviewRulerRenderer extends Disposable { * and hide the canvas if the alt buffer is active */ private _registerBufferChangeListeners(): void { - this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); + this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh())); this.register(this._bufferService.buffers.onBufferActivate(() => { this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; })); diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 1acde934..187bd3b5 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -74,6 +74,7 @@ export class Terminal implements ITerminalApi { public get onScroll(): IEvent { return this._core.onScroll; } public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } + public get onWriteParsed(): IEvent { return this._core.onWriteParsed; } public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 852d8dc4..78bd7f56 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -39,8 +39,8 @@ export class RenderService extends Disposable implements IRenderService { private _onDimensionsChange = new EventEmitter(); public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } - private _onRenderedBufferChange = new EventEmitter<{ start: number, end: number }>(); - public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRenderedBufferChange.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 }>(); @@ -131,7 +131,7 @@ export class RenderService extends Disposable implements IRenderService { // Fire render event only if it was not a redraw if (!this._isNextRenderRedrawOnly) { - this._onRenderedBufferChange.fire({ start, end }); + this._onRenderedViewportChange.fire({ start, end }); } this._onRender.fire({ start, end }); this._isNextRenderRedrawOnly = true; diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 056bf91c..00534707 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -49,7 +49,7 @@ export interface IRenderService extends IDisposable { * Fires when buffer changes are rendered. This does not fire when only cursor * or selections are rendered. */ - onRenderedBufferChange: IEvent<{ start: number, end: number }>; + onRenderedViewportChange: IEvent<{ start: number, end: number }>; /** * Fires on render */ diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 12b374c8..af9ec3f9 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -68,6 +68,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { 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(); /** * Internally we track the source of the scroll but this is meaningless outside the library so * it's filtered out. @@ -138,6 +140,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { // Setup WriteBuffer this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); + this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed)); } public dispose(): void { diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index cc84c9ab..67fd751e 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -4,6 +4,8 @@ * @license MIT */ +import { EventEmitter, IEvent } from 'common/EventEmitter'; + declare const setTimeout: (handler: () => void, timeout?: number) => void; /** @@ -44,6 +46,8 @@ export class WriteBuffer { private _bufferOffset = 0; private _isSyncWriting = false; private _syncCalls = 0; + public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } + private _onWriteParsed = new EventEmitter(); constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } @@ -220,5 +224,6 @@ export class WriteBuffer { this._pendingData = 0; this._bufferOffset = 0; } + this._onWriteParsed.fire(); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 15ee4650..4ebd0254 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -839,6 +839,17 @@ declare module 'xterm' { */ onRender: IEvent<{ start: number, end: number }>; + /** + * Adds an event listener for when data has been parsed by the terminal, + * after {@link write} is called. This event is useful to listen for any + * changes in the buffer. + * + * This fires at most once per frame, after data parsing completes. Note + * that this can fire when there are still writes pending if there is a lot + * of data. + */ + onWriteParsed: IEvent; + /** * Adds an event listener for when the terminal is resized. The event value * contains the new size.