diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 2a9e09bac5..b3b5fe10ab 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -19,8 +19,9 @@ import { ICellData } from 'common/Types'; import { Terminal } from 'xterm'; import { IRenderLayer } from './Types'; import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export abstract class BaseRenderLayer implements IRenderLayer { +export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; private _scaledCharWidth: number = 0; @@ -51,6 +52,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected readonly _decorationService: IDecorationService, protected readonly _coreBrowserService: ICoreBrowserService ) { + super(); this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._selectionModel, this._decorationService, this._coreBrowserService); this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -58,11 +60,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._initCanvas(); this._container.appendChild(this._canvas); this._refreshCharAtlas(this._colors); - } - public dispose(): void { - removeElementFromParent(this._canvas); - this._charAtlas?.dispose(); + this.register(toDisposable(() => { + removeElementFromParent(this._canvas); + this._charAtlas?.dispose(); + })); } private _initCanvas(): void { diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index f90dad1553..0378670bb8 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -9,20 +9,26 @@ import { CanvasRenderer } from './CanvasRenderer'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export class CanvasAddon implements ITerminalAddon { +export class CanvasAddon extends Disposable implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: CanvasRenderer; - private readonly _onChangeTextureAtlas = new EventEmitter(); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderer?.textureAtlas; + } + public activate(terminal: Terminal): void { const core = (terminal as any)._core; if (!terminal.element) { - core.onWillOpen(() => this.activate(terminal)); + this.register(core.onWillOpen(() => this.activate(terminal))); return; } + this._terminal = terminal; const bufferService: IBufferService = core._bufferService; const renderService: IRenderService = core._renderService; @@ -35,24 +41,17 @@ export class CanvasAddon implements ITerminalAddon { const colors: IColorSet = core._colorManager.colors; const screenElement: HTMLElement = core.screenElement; const linkifier = core.linkifier2; + this._renderer = new CanvasRenderer(terminal, colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); - forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas); + this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); renderService.setRenderer(this._renderer); renderService.onResize(bufferService.cols, bufferService.rows); - } - - public dispose(): void { - if (!this._terminal) { - throw new Error('Cannot dispose CanvasAddon because it is activated'); - } - const renderService: IRenderService = (this._terminal as any)._core._renderService; - renderService.setRenderer((this._terminal as any)._core._createRenderer()); - renderService.onResize(this._terminal.cols, this._terminal.rows); - this._renderer?.dispose(); - this._renderer = undefined; - } - public get textureAtlas(): HTMLCanvasElement | undefined { - return this._renderer?.textureAtlas; + this.register(toDisposable(() => { + renderService.setRenderer((this._terminal as any)._core._createRenderer()); + renderService.onResize(terminal.cols, terminal.rows); + this._renderer?.dispose(); + this._renderer = undefined; + })); } } diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index eb1c085ff8..b4e41282a2 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -9,7 +9,7 @@ import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/rende import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; import { IColorSet, ILinkifier2 } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { Terminal } from 'xterm'; import { CursorRenderLayer } from './CursorRenderLayer'; @@ -24,9 +24,9 @@ export class CanvasRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - private readonly _onRequestRedraw = new EventEmitter(); + private readonly _onRequestRedraw = this.register(new EventEmitter()); public readonly onRequestRedraw = this._onRequestRedraw.event; - private readonly _onChangeTextureAtlas = new EventEmitter(); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; constructor( @@ -70,14 +70,13 @@ export class CanvasRenderer extends Disposable implements IRenderer { this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this.onOptionsChanged(); - } - public dispose(): void { - for (const l of this._renderLayers) { - l.dispose(); - } - super.dispose(); - removeTerminalFromCache(this._terminal); + this.register(toDisposable(() => { + for (const l of this._renderLayers) { + l.dispose(); + } + removeTerminalFromCache(this._terminal); + })); } public get textureAtlas(): HTMLCanvasElement | undefined { diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 78dc543222..9c3ce718c6 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -12,6 +12,7 @@ import { IBufferService, IOptionsService, ICoreService, IDecorationService } fro import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; import { Terminal } from 'xterm'; +import { toDisposable } from 'common/Lifecycle'; interface ICursorState { x: number; @@ -57,14 +58,10 @@ export class CursorRenderLayer extends BaseRenderLayer { 'block': this._renderBlockCursor.bind(this), 'underline': this._renderUnderlineCursor.bind(this) }; - } - - public dispose(): void { - if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.dispose(); + this.register(toDisposable(() => { + this._cursorBlinkStateManager?.dispose(); this._cursorBlinkStateManager = undefined; - } - super.dispose(); + })); } public resize(dim: IRenderDimensions): void { diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts index db622b3fe7..da9ebe16b2 100644 --- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -28,8 +28,8 @@ export class LinkRenderLayer extends BaseRenderLayer { ) { super(terminal, container, 'link', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); - linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); - linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); + this.register(linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e))); + this.register(linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e))); } public resize(dim: IRenderDimensions): void { diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 8d80514b43..8d5508e688 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -293,19 +293,4 @@ export class TextRenderLayer extends BaseRenderLayer { this._characterOverlapCache[chars] = overlaps; return overlaps; } - - /** - * Clear the charcater at the cell specified. - * @param x The column of the char. - * @param y The row of the char. - */ - // private _clearChar(x: number, y: number): void { - // let colsToClear = 1; - // // Clear the adjacent character if it was wide - // const state = this._state.cache[x][y]; - // if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { - // colsToClear = 2; - // } - // this.clearCells(x, y, colsToClear, 1); - // } } diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index 0527eae9db..75c5ccc962 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -41,7 +41,6 @@ export interface IRenderer extends IDisposable { */ readonly onRequestRedraw: IEvent; - dispose(): void; setColors(colors: IColorSet): void; onDevicePixelRatioChange(): void; onResize(cols: number, rows: number): void; diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 689899ef19..249dd5947e 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -5,6 +5,7 @@ import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm'; import { EventEmitter } from 'common/EventEmitter'; +import { Disposable, toDisposable } from 'common/Lifecycle'; export interface ISearchOptions { regex?: boolean; @@ -50,7 +51,7 @@ type LineCacheEntry = [ const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs -export class SearchAddon implements ITerminalAddon { +export class SearchAddon extends Disposable implements ITerminalAddon { private _terminal: Terminal | undefined; private _cachedSearchTerm: string | undefined; private _selectedDecoration: IDecoration | undefined; @@ -72,13 +73,18 @@ export class SearchAddon implements ITerminalAddon { private _resultIndex: number | undefined; - private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>(); + private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>()); public readonly onDidChangeResults = this._onDidChangeResults.event; public activate(terminal: Terminal): void { this._terminal = terminal; - this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches()); - this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches()); + this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches())); + this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches())); + this.register(toDisposable(() => { + this.clearDecorations(); + this._onDataDisposable?.dispose(); + this._onResizeDisposable?.dispose(); + })); } private _updateMatches(): void { @@ -94,12 +100,6 @@ export class SearchAddon implements ITerminalAddon { } } - public dispose(): void { - this.clearDecorations(); - this._onDataDisposable?.dispose(); - this._onResizeDisposable?.dispose(); - } - public clearDecorations(retainCachedSearchTerm?: boolean): void { this._selectedDecoration?.dispose(); this._searchResults?.clear(); diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index f27ef29ffc..e04f081c49 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -76,7 +76,7 @@ let $glyph: IRasterizedGlyph | undefined = undefined; let $leftCellPadding = 0; let $clippedPixels = 0; -export class GlyphRenderer extends Disposable { +export class GlyphRenderer extends Disposable { private _atlas: ITextureAtlas | undefined; private _program: WebGLProgram; @@ -99,7 +99,6 @@ export class GlyphRenderer extends Disposable { constructor( private _terminal: Terminal, - private _colors: IColorSet, private _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions ) { diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index e51ad8658b..0f00c100db 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -10,29 +10,32 @@ import { IColorSet } from 'browser/Types'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { isSafari } from 'common/Platform'; import { ICoreService, IDecorationService } from 'common/services/Services'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export class WebglAddon implements ITerminalAddon { +export class WebglAddon extends Disposable implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private readonly _onChangeTextureAtlas = new EventEmitter(); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; - private readonly _onContextLoss = new EventEmitter(); + private readonly _onContextLoss = this.register(new EventEmitter()); public readonly onContextLoss = this._onContextLoss.event; constructor( private _preserveDrawingBuffer?: boolean - ) {} + ) { + super(); + } public activate(terminal: Terminal): void { + if (isSafari) { + throw new Error('Webgl is not currently supported on Safari'); + } const core = (terminal as any)._core; if (!terminal.element) { - core.onWillOpen(() => this.activate(terminal)); + this.register(core.onWillOpen(() => this.activate(terminal))); return; } - if (isSafari) { - throw new Error('Webgl is not currently supported on Safari'); - } this._terminal = terminal; const renderService: IRenderService = core._renderService; const characterJoinerService: ICharacterJoinerService = core._characterJoinerService; @@ -40,21 +43,16 @@ export class WebglAddon implements ITerminalAddon { const coreService: ICoreService = core.coreService; const decorationService: IDecorationService = core._decorationService; const colors: IColorSet = 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); + this._renderer = this.register(new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer)); + this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); + this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); renderService.setRenderer(this._renderer); - } - public dispose(): void { - if (!this._terminal) { - throw new Error('Cannot dispose WebglAddon because it is activated'); - } - const renderService: IRenderService = (this._terminal as any)._core._renderService; - renderService.setRenderer((this._terminal as any)._core._createRenderer()); - renderService.onResize(this._terminal.cols, this._terminal.rows); - this._renderer?.dispose(); - this._renderer = undefined; + this.register(toDisposable(() => { + const renderService: IRenderService = (this._terminal as any)._core._renderService; + renderService.setRenderer((this._terminal as any)._core._createRenderer()); + renderService.onResize(terminal.cols, terminal.rows); + })); } public get textureAtlas(): HTMLCanvasElement | undefined { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index c3bb0616c5..a554085efd 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -14,7 +14,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { EventEmitter } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { ICoreService, IDecorationService } from 'common/services/Services'; import { CharData, IBufferLine, ICellData } from 'common/Types'; import { Terminal } from 'xterm'; @@ -46,11 +46,11 @@ export class WebglRenderer extends Disposable implements IRenderer { private _isAttached: boolean; private _contextRestorationTimeout: number | undefined; - private readonly _onChangeTextureAtlas = new EventEmitter(); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; - private readonly _onRequestRedraw = new EventEmitter(); + private readonly _onRequestRedraw = this.register(new EventEmitter()); public readonly onRequestRedraw = this._onRequestRedraw.event; - private readonly _onContextLoss = new EventEmitter(); + private readonly _onContextLoss = this.register(new EventEmitter()); public readonly onContextLoss = this._onContextLoss.event; constructor( @@ -131,15 +131,14 @@ export class WebglRenderer extends Disposable implements IRenderer { this._initializeWebGLState(); this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!); - } - public dispose(): void { - for (const l of this._renderLayers) { - l.dispose(); - } - this._canvas.parentElement?.removeChild(this._canvas); - removeTerminalFromCache(this._terminal); - super.dispose(); + this.register(toDisposable(() => { + for (const l of this._renderLayers) { + l.dispose(); + } + this._canvas.parentElement?.removeChild(this._canvas); + removeTerminalFromCache(this._terminal); + })); } public get textureAtlas(): HTMLCanvasElement | undefined { @@ -255,8 +254,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._rectangleRenderer?.dispose(); this._glyphRenderer?.dispose(); - this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); - this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions); + this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions)); + this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions)); // Update dimensions and acquire char atlas this.onCharSizeChanged(); diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 07eca0bfbd..58e23fc242 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -12,8 +12,9 @@ import { ICoreBrowserService } from 'browser/services/Services'; import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { CellData } from 'common/buffer/CellData'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export abstract class BaseRenderLayer implements IRenderLayer { +export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; private _scaledCharWidth: number = 0; @@ -33,18 +34,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _colors: IColorSet, protected readonly _coreBrowserService: ICoreBrowserService ) { + super(); this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); this._container.appendChild(this._canvas); - } - - public dispose(): void { - this._canvas.remove(); - if (this._charAtlas) { - this._charAtlas.dispose(); - } + this.register(toDisposable(() => { + this._canvas.remove(); + this._charAtlas?.dispose(); + })); } private _initCanvas(): void { diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 522f6fb622..3276a1d8b7 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -12,6 +12,7 @@ import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/ import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; import { ICoreService } from 'common/services/Services'; +import { toDisposable } from 'common/Lifecycle'; interface ICursorState { x: number; @@ -55,12 +56,10 @@ export class CursorRenderLayer extends BaseRenderLayer { 'underline': this._renderUnderlineCursor.bind(this) }; this.onOptionsChanged(terminal); - } - - public override dispose(): void { - this._cursorBlinkStateManager?.dispose(); - this._cursorBlinkStateManager = undefined; - super.dispose(); + this.register(toDisposable(() => { + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; + })); } public resize(terminal: Terminal, dim: IRenderDimensions): void { diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index dbf2fdcff5..1524634822 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -10,6 +10,7 @@ import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService } from 'browser/services/Services'; import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; +import { toDisposable } from 'common/Lifecycle'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -23,8 +24,8 @@ export class LinkRenderLayer extends BaseRenderLayer { ) { super(container, 'link', zIndex, true, colors, coreBrowserService); - terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); - terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); + this.register(terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e))); + this.register(terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e))); } public resize(terminal: Terminal, dim: IRenderDimensions): void { diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index eba283d54c..d1df4075ee 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -9,7 +9,7 @@ import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { IRenderService } from 'browser/services/Services'; import { removeElementFromParent } from 'browser/Dom'; @@ -104,12 +104,10 @@ export class AccessibilityManager extends Disposable { // This shouldn't be needed on modern browsers but is present in case the // media query that drives the ScreenDprMonitor isn't supported this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); - } - - public dispose(): void { - super.dispose(); - removeElementFromParent(this._accessibilityTreeRoot); - this._rowElements.length = 0; + this.register(toDisposable(() => { + removeElementFromParent(this._accessibilityTreeRoot); + this._rowElements.length = 0; + })); } private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index cf5b9dd2e1..c0fc26415b 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle'; +import { Disposable, getDisposeArrayDisposable, disposeArray, toDisposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; export class Linkifier2 extends Disposable implements ILinkifier2 { @@ -36,11 +36,9 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { ) { super(); this.register(getDisposeArrayDisposable(this._linkCacheDisposables)); - } - - public dispose(): void { - super.dispose(); - this._lastMouseEvent = undefined; + this.register(toDisposable(() => { + this._lastMouseEvent = undefined; + })); } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { diff --git a/src/browser/ScreenDprMonitor.ts b/src/browser/ScreenDprMonitor.ts index 8129da0768..1c3f31b753 100644 --- a/src/browser/ScreenDprMonitor.ts +++ b/src/browser/ScreenDprMonitor.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void; @@ -26,6 +26,9 @@ export class ScreenDprMonitor extends Disposable { constructor(private _parentWindow: Window) { super(); this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; + this.register(toDisposable(() => { + this.clearListener(); + })); } public setListener(listener: ScreenDprListener): void { @@ -43,11 +46,6 @@ export class ScreenDprMonitor extends Disposable { this._updateDpr(); } - public dispose(): void { - super.dispose(); - this.clearListener(); - } - private _updateDpr(): void { if (!this._outerListener) { return; diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index eaf420eda0..a9d0b772ac 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -1378,7 +1378,6 @@ describe('Terminal', () => { assert.deepEqual(disposeStack, [markers[0], markers[1]]); // trimmed marker objs should be disposed assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]); - assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]); // trimmed markers should contain line -1 assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); }); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 14dbca3815..fef92d3d42 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -56,6 +56,7 @@ import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; import { OscLinkProvider } from 'browser/OscLinkProvider'; +import { toDisposable } from 'common/Lifecycle'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -122,28 +123,28 @@ export class Terminal extends CoreTerminal implements ITerminal { private _colorManager: ColorManager | undefined; private _theme: ITheme | undefined; - private readonly _onCursorMove = new EventEmitter(); + private readonly _onCursorMove = this.register(new EventEmitter()); public readonly onCursorMove = this._onCursorMove.event; - private readonly _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); + private readonly _onKey = this.register(new EventEmitter<{ key: string, domEvent: KeyboardEvent }>()); public readonly onKey = this._onKey.event; - private readonly _onRender = new EventEmitter<{ start: number, end: number }>(); + private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>()); public readonly onRender = this._onRender.event; - private readonly _onSelectionChange = new EventEmitter(); + private readonly _onSelectionChange = this.register(new EventEmitter()); public readonly onSelectionChange = this._onSelectionChange.event; - private readonly _onTitleChange = new EventEmitter(); + private readonly _onTitleChange = this.register(new EventEmitter()); public readonly onTitleChange = this._onTitleChange.event; - private readonly _onBell = new EventEmitter(); + private readonly _onBell = this.register(new EventEmitter()); public readonly onBell = this._onBell.event; - private _onFocus = new EventEmitter(); + private _onFocus = this.register(new EventEmitter()); public get onFocus(): IEvent { return this._onFocus.event; } - private _onBlur = new EventEmitter(); + private _onBlur = this.register(new EventEmitter()); public get onBlur(): IEvent { return this._onBlur.event; } - private _onA11yCharEmitter = new EventEmitter(); + private _onA11yCharEmitter = this.register(new EventEmitter()); public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } - private _onA11yTabEmitter = new EventEmitter(); + private _onA11yTabEmitter = this.register(new EventEmitter()); public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } - private _onWillOpen = new EventEmitter(); + private _onWillOpen = this.register(new EventEmitter()); public get onWillOpen(): IEvent { return this._onWillOpen.event; } /** @@ -184,6 +185,11 @@ export class Terminal extends CoreTerminal implements ITerminal { // Setup listeners this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); + + this.register(toDisposable(() => { + this._customKeyEventHandler = undefined; + this.element?.parentNode?.removeChild(this.element); + })); } /** @@ -235,17 +241,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.viewport?.onThemeChange(this._colorManager.colors); } - public dispose(): void { - if (this._isDisposed) { - return; - } - super.dispose(); - this._renderService?.dispose(); - this._customKeyEventHandler = undefined; - this.write = () => { }; - this.element?.parentNode?.removeChild(this.element); - } - protected _setup(): void { super._setup(); diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index 7fcc5ea954..5836e26651 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -5,7 +5,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IRenderService } from 'browser/services/Services'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services'; export class BufferDecorationRenderer extends Disposable { @@ -39,12 +39,10 @@ export class BufferDecorationRenderer extends Disposable { })); this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh())); this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration))); - } - - public override dispose(): void { - this._container.remove(); - this._decorationElements.clear(); - super.dispose(); + this.register(toDisposable(() => { + this._container.remove(); + this._decorationElements.clear(); + })); } private _queueRefresh(): void { diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index e7db50c55d..9251d2e36b 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -6,7 +6,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; // Helper objects to avoid excessive calculation and garbage collection during rendering. These are @@ -68,6 +68,9 @@ export class OverviewRulerRenderer extends Disposable { this._registerDecorationListeners(); this._registerBufferChangeListeners(); this._registerDimensionChangeListeners(); + this.register(toDisposable(() => { + this._canvas?.remove(); + })); } /** @@ -120,11 +123,6 @@ export class OverviewRulerRenderer extends Disposable { this._queueRefresh(true); } - public override dispose(): void { - this._canvas?.remove(); - super.dispose(); - } - private _refreshDrawConstants(): void { // width const outerWidth = Math.floor(this._canvas.width / 3); diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 6a70ccec4a..b474bc7a2f 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -6,7 +6,7 @@ import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } 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'; @@ -40,7 +40,7 @@ export class DomRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - public readonly onRequestRedraw = new EventEmitter().event; + public readonly onRequestRedraw = this.register(new EventEmitter()).event; constructor( private _colors: IColorSet, @@ -89,16 +89,14 @@ export class DomRenderer extends Disposable implements IRenderer { this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e))); this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e))); - } - - public dispose(): void { - this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); - // Outside influences such as React unmounts may manipulate the DOM before our disposal. - // https://github.com/xtermjs/xterm.js/issues/2960 - removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement); + this.register(toDisposable(() => { + this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); - super.dispose(); + // Outside influences such as React unmounts may manipulate the DOM before our disposal. + // https://github.com/xtermjs/xterm.js/issues/2960 + removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement); + })); } private _updateDimensions(): void { diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 7062deecef..267a361b50 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -4,10 +4,11 @@ */ import { IOptionsService } from 'common/services/Services'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { EventEmitter } from 'common/EventEmitter'; import { ICharSizeService } from 'browser/services/Services'; +import { Disposable } from 'common/Lifecycle'; -export class CharSizeService implements ICharSizeService { +export class CharSizeService extends Disposable implements ICharSizeService { public serviceBrand: undefined; public width: number = 0; @@ -16,7 +17,7 @@ export class CharSizeService implements ICharSizeService { public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } - private readonly _onCharSizeChange = new EventEmitter(); + private readonly _onCharSizeChange = this.register(new EventEmitter()); public readonly onCharSizeChange = this._onCharSizeChange.event; constructor( @@ -24,6 +25,7 @@ export class CharSizeService implements ICharSizeService { parentElement: HTMLElement, @IOptionsService private readonly _optionsService: IOptionsService ) { + super(); this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 3a48281c75..a063b37f19 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -40,13 +40,13 @@ export class RenderService extends Disposable implements IRenderService { columnSelectMode: false }; - private readonly _onDimensionsChange = new EventEmitter(); + private readonly _onDimensionsChange = this.register(new EventEmitter()); public readonly onDimensionsChange = this._onDimensionsChange.event; - private readonly _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>(); + private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>()); public readonly onRenderedViewportChange = this._onRenderedViewportChange.event; - private readonly _onRender = new EventEmitter<{ start: number, end: number }>(); + private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>()); public readonly onRender = this._onRender.event; - private readonly _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); + private readonly _onRefreshRequest = this.register(new EventEmitter<{ start: number, end: number }>()); public readonly onRefreshRequest = this._onRefreshRequest.event; public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; } @@ -169,10 +169,6 @@ export class RenderService extends Disposable implements IRenderService { this._onDimensionsChange.fire(this._renderer.dimensions); } - public dispose(): void { - super.dispose(); - } - public hasRenderer(): boolean { return !!this._renderer; } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 3780c7ee8f..130537d155 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -15,7 +15,7 @@ import { IBufferRange, ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { getRangeLength } from 'common/buffer/BufferRange'; /** @@ -148,10 +148,10 @@ export class SelectionService extends Disposable implements ISelectionService { this._model = new SelectionModel(this._bufferService); this._activeSelectionMode = SelectionMode.NORMAL; - } - public dispose(): void { - this._removeMouseDownListeners(); + this.register(toDisposable(() => { + this._removeMouseDownListeners(); + })); } public reset(): void { diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 599db1aba8..b7e1e07519 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -5,6 +5,7 @@ import { ICircularList } from 'common/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; export interface IInsertEvent { index: number; @@ -20,21 +21,22 @@ export interface IDeleteEvent { * Represents a circular list; a list with a maximum size that wraps around when push is called, * overriding values at the start of the list. */ -export class CircularList implements ICircularList { +export class CircularList extends Disposable implements ICircularList { protected _array: (T | undefined)[]; private _startIndex: number; private _length: number; - public readonly onDeleteEmitter = new EventEmitter(); + public readonly onDeleteEmitter = this.register(new EventEmitter()); public readonly onDelete = this.onDeleteEmitter.event; - public readonly onInsertEmitter = new EventEmitter(); + public readonly onInsertEmitter = this.register(new EventEmitter()); public readonly onInsert = this.onInsertEmitter.event; - public readonly onTrimEmitter = new EventEmitter(); + public readonly onTrimEmitter = this.register(new EventEmitter()); public readonly onTrim = this.onTrimEmitter.event; constructor( private _maxLength: number ) { + super(); this._array = new Array(this._maxLength); this._startIndex = 0; this._length = 0; diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 2ad6f73515..ea2528dba7 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; 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'; @@ -59,15 +59,15 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - private readonly _onBinary = new EventEmitter(); + private readonly _onBinary = this.register(new EventEmitter()); public readonly onBinary = this._onBinary.event; - private readonly _onData = new EventEmitter(); + private readonly _onData = this.register(new EventEmitter()); public readonly onData = this._onData.event; - protected _onLineFeed = new EventEmitter(); + protected _onLineFeed = this.register(new EventEmitter()); public readonly onLineFeed = this._onLineFeed.event; - private readonly _onResize = new EventEmitter<{ cols: number, rows: number }>(); + private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>()); public readonly onResize = this._onResize.event; - protected readonly _onWriteParsed = new EventEmitter(); + protected readonly _onWriteParsed = this.register(new EventEmitter()); public readonly onWriteParsed = this._onWriteParsed.event; /** @@ -75,13 +75,13 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * it's filtered out. */ protected _onScrollApi?: EventEmitter; - protected _onScroll = new EventEmitter(); + protected _onScroll = this.register(new EventEmitter()); public get onScroll(): IEvent { if (!this._onScrollApi) { - this._onScrollApi = new EventEmitter(); - this.register(this._onScroll.event(ev => { + this._onScrollApi = this.register(new EventEmitter()); + this._onScroll.event(ev => { this._onScrollApi?.fire(ev.position); - })); + }); } return this._onScrollApi.event; } @@ -103,17 +103,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { // Setup and initialize services this._instantiationService = new InstantiationService(); - this.optionsService = new OptionsService(options); + this.optionsService = this.register(new OptionsService(options)); this._instantiationService.setService(IOptionsService, this.optionsService); this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); this._instantiationService.setService(IBufferService, this._bufferService); - this._logService = this._instantiationService.createInstance(LogService); + this._logService = this.register(this._instantiationService.createInstance(LogService)); this._instantiationService.setService(ILogService, this._logService); this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); this._instantiationService.setService(ICoreService, this.coreService); - this.coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this.coreMouseService = this.register(this._instantiationService.createInstance(CoreMouseService)); this._instantiationService.setService(ICoreMouseService, this.coreMouseService); - this.unicodeService = this._instantiationService.createInstance(UnicodeService); + this.unicodeService = this.register(this._instantiationService.createInstance(UnicodeService)); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); this._instantiationService.setService(ICharsetService, this._charsetService); @@ -121,7 +121,7 @@ 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._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService); + this._inputHandler = this.register(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); @@ -141,17 +141,13 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { })); // Setup WriteBuffer - this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); + this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult))); this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed)); - } - public dispose(): void { - if (this._isDisposed) { - return; - } - super.dispose(); - this._windowsMode?.dispose(); - this._windowsMode = undefined; + this.register(toDisposable(() => { + this._windowsMode?.dispose(); + this._windowsMode = undefined; + })); } public write(data: string | Uint8Array, callback?: () => void): void { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 7f0879efc3..b91b446c5f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -132,32 +132,32 @@ export class InputHandler extends Disposable implements IInputHandler { private _activeBuffer: IBuffer; - private readonly _onRequestBell = new EventEmitter(); + private readonly _onRequestBell = this.register(new EventEmitter()); public readonly onRequestBell = this._onRequestBell.event; - private readonly _onRequestRefreshRows = new EventEmitter(); + private readonly _onRequestRefreshRows = this.register(new EventEmitter()); public readonly onRequestRefreshRows = this._onRequestRefreshRows.event; - private readonly _onRequestReset = new EventEmitter(); + private readonly _onRequestReset = this.register(new EventEmitter()); public readonly onRequestReset = this._onRequestReset.event; - private readonly _onRequestSendFocus = new EventEmitter(); + private readonly _onRequestSendFocus = this.register(new EventEmitter()); public readonly onRequestSendFocus = this._onRequestSendFocus.event; - private readonly _onRequestSyncScrollBar = new EventEmitter(); + private readonly _onRequestSyncScrollBar = this.register(new EventEmitter()); public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event; - private readonly _onRequestWindowsOptionsReport = new EventEmitter(); + private readonly _onRequestWindowsOptionsReport = this.register(new EventEmitter()); public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event; - private readonly _onA11yChar = new EventEmitter(); + private readonly _onA11yChar = this.register(new EventEmitter()); public readonly onA11yChar = this._onA11yChar.event; - private readonly _onA11yTab = new EventEmitter(); + private readonly _onA11yTab = this.register(new EventEmitter()); public readonly onA11yTab = this._onA11yTab.event; - private readonly _onCursorMove = new EventEmitter(); + private readonly _onCursorMove = this.register(new EventEmitter()); public readonly onCursorMove = this._onCursorMove.event; - private readonly _onLineFeed = new EventEmitter(); + private readonly _onLineFeed = this.register(new EventEmitter()); public readonly onLineFeed = this._onLineFeed.event; - private readonly _onScroll = new EventEmitter(); + private readonly _onScroll = this.register(new EventEmitter()); public readonly onScroll = this._onScroll.event; - private readonly _onTitleChange = new EventEmitter(); + private readonly _onTitleChange = this.register(new EventEmitter()); public readonly onTitleChange = this._onTitleChange.event; - private readonly _onColor = new EventEmitter(); + private readonly _onColor = this.register(new EventEmitter()); public readonly onColor = this._onColor.event; private _parseStack: IParseStack = { @@ -382,10 +382,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params))); } - public dispose(): void { - super.dispose(); - } - /** * Async parse support. */ diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts index b3a7cc2189..7ccc8aa7b3 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -17,15 +17,18 @@ export abstract class Disposable implements IDisposable { } /** - * Disposes the object, triggering the `dispose` method on all registered IDisposables. + * Disposes the object, triggering the `dispose` method on all registered IDisposables. This is a + * readonly property instead of a method to prevent subclasses overriding it which is an easy + * mistake that can introduce memory leaks. If a class extends Disposable, all dispose calls + * should be done via {@link register}. */ - public dispose(): void { + public readonly dispose = (): void => { this._isDisposed = true; for (const d of this._disposables) { d.dispose(); } this._disposables.length = 0; - } + }; /** * Registers a disposable object. diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index 56d64a7262..0629e26a66 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -3,25 +3,25 @@ * @license MIT */ -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; -import { IMarker } from 'common/Types'; +import { EventEmitter } from 'common/EventEmitter'; +import { disposeArray } from 'common/Lifecycle'; +import { IDisposable, IMarker } from 'common/Types'; -export class Marker extends Disposable implements IMarker { +export class Marker implements IMarker { private static _nextId = 1; - private _id: number = Marker._nextId++; public isDisposed: boolean = false; + private _disposables: IDisposable[] = []; + private _id: number = Marker._nextId++; public get id(): number { return this._id; } - private readonly _onDispose = new EventEmitter(); + private readonly _onDispose = this.register(new EventEmitter()); public readonly onDispose = this._onDispose.event; constructor( public line: number ) { - super(); } public dispose(): void { @@ -32,6 +32,12 @@ export class Marker extends Disposable implements IMarker { this.line = -1; // Emit before super.dispose such that dispose listeners get a change to react this._onDispose.fire(); - super.dispose(); + disposeArray(this._disposables); + this._disposables.length = 0; + } + + public register(disposable: T): T { + this._disposables.push(disposable); + return disposable; } } diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index cb40ffa1f3..68dbc6f72e 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -5,6 +5,7 @@ */ import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; declare const setTimeout: (handler: () => void, timeout?: number) => void; @@ -33,7 +34,7 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -export class WriteBuffer { +export class WriteBuffer extends Disposable { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; private _pendingData = 0; @@ -42,10 +43,12 @@ export class WriteBuffer { private _syncCalls = 0; private _didUserInput = false; - private readonly _onWriteParsed = new EventEmitter(); + private readonly _onWriteParsed = this.register(new EventEmitter()); public readonly onWriteParsed = this._onWriteParsed.event; - constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } + constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { + super(); + } public handleUserInput(): void { this._didUserInput = true; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 4b23fa0016..76e88deac9 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -87,7 +87,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { } } public mockOscParser(): void { - this._oscParser = oscPutParser; + (this as any)._oscParser = oscPutParser; } public identifier(id: IFunctionIdentifier): number { return this._identifier(id); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index f20a7e914f..5bcd2dd3b1 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -5,7 +5,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; import { Params } from 'common/parser/Params'; @@ -242,8 +242,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _executeHandlers: { [flag: number]: ExecuteHandlerType }; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; - protected _oscParser: IOscParser; - protected _dcsParser: IDcsParser; + protected readonly _oscParser: IOscParser; + protected readonly _dcsParser: IDcsParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers @@ -284,8 +284,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlers = Object.create(null); this._csiHandlers = Object.create(null); this._escHandlers = Object.create(null); - this._oscParser = new OscParser(); - this._dcsParser = new DcsParser(); + this.register(toDisposable(() => { + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); + this._escHandlers = Object.create(null); + })); + this._oscParser = this.register(new OscParser()); + this._dcsParser = this.register(new DcsParser()); this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) @@ -338,14 +343,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP return res.reverse().join(''); } - public dispose(): void { - this._csiHandlers = Object.create(null); - this._executeHandlers = Object.create(null); - this._escHandlers = Object.create(null); - this._oscParser.dispose(); - this._dcsParser.dispose(); - } - public setPrintHandler(handler: PrintHandlerType): void { this._printHandler = handler; } diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts index 033f5955d9..9e49ce2b4f 100644 --- a/src/common/public/BufferNamespaceApi.ts +++ b/src/common/public/BufferNamespaceApi.ts @@ -5,7 +5,7 @@ import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm'; import { BufferApiView } from 'common/public/BufferApiView'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { EventEmitter } from 'common/EventEmitter'; import { ICoreTerminal } from 'common/Types'; export class BufferNamespaceApi implements IBufferNamespaceApi { diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 1bc93041c9..f238206c8a 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -22,9 +22,9 @@ export class BufferService extends Disposable implements IBufferService { /** Whether the user is scrolling (locks the scroll position) */ public isUserScrolling: boolean = false; - private readonly _onResize = new EventEmitter<{ cols: number, rows: number }>(); + private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>()); public readonly onResize = this._onResize.event; - private readonly _onScroll = new EventEmitter(); + private readonly _onScroll = this.register(new EventEmitter()); public readonly onScroll = this._onScroll.event; public get buffer(): IBuffer { return this.buffers.active; } @@ -36,12 +36,7 @@ export class BufferService extends Disposable implements IBufferService { super(); this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS); this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS); - this.buffers = new BufferSet(optionsService, this); - } - - public dispose(): void { - super.dispose(); - this.buffers.dispose(); + this.buffers = this.register(new BufferSet(optionsService, this)); } public resize(cols: number, rows: number): void { diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 8c2a24def6..d955370e55 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -5,6 +5,7 @@ import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; +import { Disposable } from 'common/Lifecycle'; /** * Supported default protocols. @@ -165,20 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = { * a tracking report to the backend based on protocol and encoding limitations. * To send a mouse event call `triggerMouseEvent`. */ -export class CoreMouseService implements ICoreMouseService { +export class CoreMouseService extends Disposable implements ICoreMouseService { private _protocols: { [name: string]: ICoreMouseProtocol } = {}; private _encodings: { [name: string]: CoreMouseEncoding } = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; private _lastEvent: ICoreMouseEvent | null = null; - private readonly _onProtocolChange = new EventEmitter(); + private readonly _onProtocolChange = this.register(new EventEmitter()); public readonly onProtocolChange = this._onProtocolChange.event; constructor( @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService ) { + super(); // register default protocols and encodings for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]); for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]); diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 522b04deae..c27e7b2f53 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -5,7 +5,7 @@ import { css } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; import { SortedList } from 'common/SortedList'; import { IColor } from 'common/Types'; @@ -32,6 +32,16 @@ export class DecorationService extends Disposable implements IDecorationService public get decorations(): IterableIterator { return this._decorations.values(); } + constructor() { + super(); + + this.register(toDisposable(() => { + for (const d of this._decorations.values()) { + this._onDecorationRemoved.fire(d); + } + this.reset(); + })); + } public registerDecoration(options: IDecorationOptions): IDecoration | undefined { if (options.marker.isDisposed) { return undefined; @@ -81,25 +91,19 @@ export class DecorationService extends Disposable implements IDecorationService } }); } - - public dispose(): void { - for (const d of this._decorations.values()) { - this._onDecorationRemoved.fire(d); - } - this.reset(); - } } class Decoration extends Disposable implements IInternalDecoration { public readonly marker: IMarker; public element: HTMLElement | undefined; - public isDisposed: boolean = false; public readonly onRenderEmitter = this.register(new EventEmitter()); public readonly onRender = this.onRenderEmitter.event; private readonly _onDispose = this.register(new EventEmitter()); public readonly onDispose = this._onDispose.event; + public get isDisposed(): boolean { return this._isDisposed; } + private _cachedBg: IColor | undefined | null = null; public get backgroundColorRGB(): IColor | undefined { if (this._cachedBg === null) { @@ -132,14 +136,12 @@ class Decoration extends Disposable implements IInternalDecoration { if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) { this.options.overviewRulerOptions.position = 'full'; } - } - public override dispose(): void { - if (this._isDisposed) { - return; - } - this._isDisposed = true; - this._onDispose.fire(); - super.dispose(); + this.register(toDisposable(() => { + if (this._isDisposed) { + return; + } + this._onDispose.fire(); + })); } } diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index d3566567e1..854f85de37 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -3,6 +3,7 @@ * @license MIT */ +import { Disposable } from 'common/Lifecycle'; import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services'; type LogType = (message?: any, ...optionalParams: any[]) => void; @@ -29,7 +30,7 @@ const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = { const LOG_PREFIX = 'xterm.js: '; -export class LogService implements ILogService { +export class LogService extends Disposable implements ILogService { public serviceBrand: any; public logLevel: LogLevelEnum = LogLevelEnum.OFF; @@ -37,12 +38,13 @@ export class LogService implements ILogService { constructor( @IOptionsService private readonly _optionsService: IOptionsService ) { + super(); this._updateLogLevel(); - this._optionsService.onOptionChange(key => { + this.register(this._optionsService.onOptionChange(key => { if (key === 'logLevel') { this._updateLogLevel(); } - }); + })); } private _updateLogLevel(): void { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 33aa3ee239..336591e5d5 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -7,6 +7,7 @@ import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/S import { EventEmitter, IEvent } from 'common/EventEmitter'; import { isMac } from 'common/Platform'; import { CursorStyle } from 'common/Types'; +import { Disposable } from 'common/Lifecycle'; export const DEFAULT_OPTIONS: Readonly> = { cols: 80, @@ -51,16 +52,17 @@ export const DEFAULT_OPTIONS: Readonly> = { const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; -export class OptionsService implements IOptionsService { +export class OptionsService extends Disposable implements IOptionsService { public serviceBrand: any; public readonly rawOptions: Required; public options: Required; - private readonly _onOptionChange = new EventEmitter(); + private readonly _onOptionChange = this.register(new EventEmitter()); public readonly onOptionChange = this._onOptionChange.event; constructor(options: Partial) { + super(); // set the default value of each option const defaultOptions = { ...DEFAULT_OPTIONS }; for (const key in options) { diff --git a/src/common/services/UnicodeService.ts b/src/common/services/UnicodeService.ts index 239f4d6225..5c5b74f698 100644 --- a/src/common/services/UnicodeService.ts +++ b/src/common/services/UnicodeService.ts @@ -6,7 +6,6 @@ import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Servic import { EventEmitter, IEvent } from 'common/EventEmitter'; import { UnicodeV6 } from 'common/input/UnicodeV6'; - export class UnicodeService implements IUnicodeService { public serviceBrand: any; @@ -24,6 +23,10 @@ export class UnicodeService implements IUnicodeService { this._activeProvider = defaultProvider; } + public dispose(): void { + this._onChange.dispose(); + } + public get versions(): string[] { return Object.keys(this._providers); } diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 639988ebd8..f021c42bfb 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -32,15 +32,15 @@ export class Terminal extends CoreTerminal { // TODO: We should remove options once components adopt optionsService public get options(): Required { return this.optionsService.options; } - private readonly _onBell = new EventEmitter(); + private readonly _onBell = this.register(new EventEmitter()); public readonly onBell = this._onBell.event; - private readonly _onCursorMove = new EventEmitter(); + private readonly _onCursorMove = this.register(new EventEmitter()); public readonly onCursorMove = this._onCursorMove.event; - private readonly _onTitleChange = new EventEmitter(); + private readonly _onTitleChange = this.register(new EventEmitter()); public readonly onTitleChange = this._onTitleChange.event; - private readonly _onA11yCharEmitter = new EventEmitter(); + private readonly _onA11yCharEmitter = this.register(new EventEmitter()); public readonly onA11yChar = this._onA11yCharEmitter.event; - private readonly _onA11yTabEmitter = new EventEmitter(); + private readonly _onA11yTabEmitter = this.register(new EventEmitter()); public readonly onA11yTab = this._onA11yTabEmitter.event; /** @@ -71,14 +71,6 @@ export class Terminal extends CoreTerminal { this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); } - public dispose(): void { - if (this._isDisposed) { - return; - } - super.dispose(); - this.write = () => { }; - } - /** * Convenience property to active buffer. */