diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index 167e93a791..2712a1d10e 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import type { ITerminalAddon, IDisposable } from '@xterm/xterm'; +import type { ITerminalAddon, IDisposable, ISharedExports } from '@xterm/xterm'; import type { ImageAddon as IImageApi } from '@xterm/addon-image'; import { IIPHandler } from './IIPHandler'; import { ImageRenderer } from './ImageRenderer'; @@ -57,7 +57,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi { private _terminal: ITerminalExt | undefined; private _handlers: Map = new Map(); - constructor(opts?: Partial) { + constructor(private _sharedExports: ISharedExports, opts?: Partial) { this._opts = Object.assign({}, DEFAULT_OPTIONS, opts); this._defaultOpts = Object.assign({}, DEFAULT_OPTIONS, opts); } @@ -80,7 +80,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi { this._terminal = terminal; // internal data structures - this._renderer = new ImageRenderer(terminal); + this._renderer = new ImageRenderer(this._sharedExports, terminal); this._storage = new ImageStorage(terminal, this._renderer, this._opts); // enable size reports diff --git a/addons/addon-image/src/ImageRenderer.ts b/addons/addon-image/src/ImageRenderer.ts index e1790f4783..3dedb42453 100644 --- a/addons/addon-image/src/ImageRenderer.ts +++ b/addons/addon-image/src/ImageRenderer.ts @@ -4,9 +4,9 @@ */ import { toRGBA8888 } from 'sixel/lib/Colors'; -import { IDisposable } from '@xterm/xterm'; +import { IDisposable, IMutableDisposable, ISharedExports } from '@xterm/xterm'; import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types'; -import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; const PLACEHOLDER_LENGTH = 4096; const PLACEHOLDER_HEIGHT = 24; @@ -17,12 +17,12 @@ const PLACEHOLDER_HEIGHT = 24; * - add canvas layer to DOM (browser only for now) * - draw image tiles onRender */ -export class ImageRenderer extends Disposable implements IDisposable { +export class ImageRenderer extends AddonDisposable implements IDisposable { public canvas: HTMLCanvasElement | undefined; private _ctx: CanvasRenderingContext2D | null | undefined; private _placeholder: HTMLCanvasElement | undefined; private _placeholderBitmap: ImageBitmap | undefined; - private _optionsRefresh = this._register(new MutableDisposable()); + private _optionsRefresh: IMutableDisposable; private _oldOpen: ((parent: HTMLElement) => void) | undefined; private _renderService: IRenderService | undefined; private _oldSetRenderer: ((renderer: any) => void) | undefined; @@ -67,8 +67,8 @@ export class ImageRenderer extends Disposable implements IDisposable { } - constructor(private _terminal: ITerminalExt) { - super(); + constructor(sharedExports: ISharedExports, private _terminal: ITerminalExt) { + super(sharedExports); this._oldOpen = this._terminal._core.open; this._terminal._core.open = (parent: HTMLElement): void => { this._oldOpen?.call(this._terminal._core, parent); @@ -78,13 +78,14 @@ export class ImageRenderer extends Disposable implements IDisposable { this._open(); } // hack to spot fontSize changes + this._optionsRefresh = new sharedExports.MutableDisposable(); this._optionsRefresh.value = this._terminal._core.optionsService.onOptionChange(option => { if (option === 'fontSize') { this.rescaleCanvas(); this._renderService?.refreshRows(0, this._terminal.rows); } }); - this._register(toDisposable(() => { + this._register(sharedExports.toDisposable(() => { this.removeLayerFromDom(); if (this._terminal._core && this._oldOpen) { this._terminal._core.open = this._oldOpen; diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index 25d47c8242..bdba47c8ad 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -8,6 +8,7 @@ import { readFileSync } from 'fs'; import { FINALIZER, introducer, sixelEncode } from 'sixel'; import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; import { deepStrictEqual, ok, strictEqual } from 'assert'; +import { ISharedExports } from '@xterm/xterm'; /** * Plugin ctor options. @@ -27,7 +28,7 @@ export interface IImageAddonOptions { // eslint-disable-next-line declare const ImageAddon: { - new(options?: Partial): any; + new(sharedExports: ISharedExports, options?: Partial): any; }; interface ITestData { @@ -91,7 +92,7 @@ test.describe('ImageAddon', () => { await ctx.page.evaluate(` window.term.reset() window.imageAddon?.dispose(); - window.imageAddon = new ImageAddon({ sixelPaletteLimit: 512 }); + window.imageAddon = new ImageAddon(sharedExports, { sixelPaletteLimit: 512 }); window.term.loadAddon(window.imageAddon); `); }); @@ -152,7 +153,7 @@ test.describe('ImageAddon', () => { iipSizeLimit: 1000 }; await ctx.page.evaluate(opts => { - (window as any).imageAddonCustom = new ImageAddon(opts.opts); + (window as any).imageAddonCustom = new ImageAddon((window as any).sharedExports, opts.opts); (window as any).term.loadAddon((window as any).imageAddonCustom); }, { opts: customSettings }); deepStrictEqual(await ctx.page.evaluate(`window.imageAddonCustom._opts`), customSettings); diff --git a/addons/addon-image/typings/addon-image.d.ts b/addons/addon-image/typings/addon-image.d.ts index 48ba488cfa..942b58c4f3 100644 --- a/addons/addon-image/typings/addon-image.d.ts +++ b/addons/addon-image/typings/addon-image.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, ISharedExports } from '@xterm/xterm'; declare module '@xterm/addon-image' { export interface IImageAddonOptions { @@ -79,7 +79,7 @@ declare module '@xterm/addon-image' { } export class ImageAddon implements ITerminalAddon { - constructor(options?: IImageAddonOptions); + constructor(sharedExports: ISharedExports, options?: IImageAddonOptions); public activate(terminal: Terminal): void; public dispose(): void; diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index 175d7af0ab..a600844412 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -3,9 +3,8 @@ * @license MIT */ -import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IDisposable, IEmitter, IEvent, ISharedExports } from '@xterm/xterm'; import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress'; -import type { Emitter, Event } from 'vs/base/common/event'; const enum ProgressType { @@ -37,9 +36,13 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { private _seqHandler: IDisposable | undefined; private _st: ProgressType = ProgressType.REMOVE; private _pr = 0; - // HACK: This uses ! to align with the API, this should be fixed when 5283 is resolved - private _onChange!: Emitter; - public onChange!: Event; + private _onChange: IEmitter; + public onChange: IEvent; + + constructor(sharedExports: ISharedExports) { + this._onChange = new sharedExports.Emitter(); + this.onChange = this._onChange.event; + } public dispose(): void { this._seqHandler?.dispose(); @@ -81,9 +84,6 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { } return true; }); - // FIXME: borrow emitter ctor from xterm, to be changed once #5283 is resolved - this._onChange = new (terminal as any)._core._onData.constructor(); - this.onChange = this._onChange!.event; } public get progress(): IProgressState { diff --git a/addons/addon-progress/test/ProgressAddon.test.ts b/addons/addon-progress/test/ProgressAddon.test.ts index 792c0445de..e532148805 100644 --- a/addons/addon-progress/test/ProgressAddon.test.ts +++ b/addons/addon-progress/test/ProgressAddon.test.ts @@ -23,7 +23,7 @@ test.describe('ProgressAddon', () => { window.progressStack = []; window.term.reset(); window.progressAddon?.dispose(); - window.progressAddon = new ProgressAddon(); + window.progressAddon = new ProgressAddon(sharedExports); window.term.loadAddon(window.progressAddon); window.progressAddon.onChange(progress => window.progressStack.push(progress)); `); diff --git a/addons/addon-progress/typings/addon-progress.d.ts b/addons/addon-progress/typings/addon-progress.d.ts index c9431dad22..d72f29bbde 100644 --- a/addons/addon-progress/typings/addon-progress.d.ts +++ b/addons/addon-progress/typings/addon-progress.d.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IDisposable, IEvent } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IDisposable, IEvent, ISharedExports } from '@xterm/xterm'; + declare module '@xterm/addon-progress' { /** @@ -15,7 +16,7 @@ declare module '@xterm/addon-progress' { /** * Creates a new progress addon */ - constructor(); + constructor(sharedExports: ISharedExports); /** * Activates the addon diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 77cf9b16a9..dd12b1822e 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -3,10 +3,10 @@ * @license MIT */ -import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm'; +import type { Terminal, IDisposable, ITerminalAddon, IDecoration, ISharedExports, IEmitter, IEvent, IMutableDisposable } from '@xterm/xterm'; import type { SearchAddon as ISearchApi } from '@xterm/addon-search'; -import { Emitter } from 'vs/base/common/event'; -import { combinedDisposable, Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; + export interface ISearchOptions { regex?: boolean; @@ -62,12 +62,12 @@ const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs const DEFAULT_HIGHLIGHT_LIMIT = 1000; -export class SearchAddon extends Disposable implements ITerminalAddon , ISearchApi { +export class SearchAddon extends AddonDisposable implements ITerminalAddon , ISearchApi { private _terminal: Terminal | undefined; private _cachedSearchTerm: string | undefined; private _highlightedLines: Set = new Set(); private _highlightDecorations: IHighlight[] = []; - private _selectedDecoration: MutableDisposable = this._register(new MutableDisposable()); + private _selectedDecoration: IMutableDisposable; private _highlightLimit: number; private _lastSearchOptions: ISearchOptions | undefined; private _highlightTimeout: number | undefined; @@ -78,13 +78,18 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA */ private _linesCache: LineCacheEntry[] | undefined; private _linesCacheTimeoutId = 0; - private _linesCacheDisposables = new MutableDisposable(); + private _linesCacheDisposables: IMutableDisposable; + + private readonly _onDidChangeResults: IEmitter<{ resultIndex: number, resultCount: number }>; + public readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>; - private readonly _onDidChangeResults = this._register(new Emitter<{ resultIndex: number, resultCount: number }>()); - public readonly onDidChangeResults = this._onDidChangeResults.event; + constructor(private _sharedExports: ISharedExports, options?: Partial) { + super(_sharedExports); + this._onDidChangeResults = this._register(new _sharedExports.Emitter<{ resultIndex: number, resultCount: number }>()); + this.onDidChangeResults = this._onDidChangeResults.event; - constructor(options?: Partial) { - super(); + this._selectedDecoration = this._register(new _sharedExports.MutableDisposable()); + this._linesCacheDisposables = new _sharedExports.MutableDisposable(); this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT; } @@ -93,7 +98,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA this._terminal = terminal; this._register(this._terminal.onWriteParsed(() => this._updateMatches())); this._register(this._terminal.onResize(() => this._updateMatches())); - this._register(toDisposable(() => this.clearDecorations())); + this._register(this._sharedExports.toDisposable(() => this.clearDecorations())); } private _updateMatches(): void { @@ -111,7 +116,9 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA public clearDecorations(retainCachedSearchTerm?: boolean): void { this._selectedDecoration.clear(); - dispose(this._highlightDecorations); + for (let i = 0; i < this._highlightDecorations.length; ++i) { + this._highlightDecorations[i].dispose(); + } this._highlightDecorations = []; this._highlightedLines.clear(); if (!retainCachedSearchTerm) { @@ -426,11 +433,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA const terminal = this._terminal!; if (!this._linesCache) { this._linesCache = new Array(terminal.buffer.active.length); - this._linesCacheDisposables.value = combinedDisposable( + const disposables = [ terminal.onLineFeed(() => this._destroyLinesCache()), terminal.onCursorMove(() => this._destroyLinesCache()), terminal.onResize(() => this._destroyLinesCache()) - ); + ]; + this._linesCacheDisposables.value = this._sharedExports.toDisposable(() => { + for (let i = 0; i < disposables.length; ++i) disposables[i].dispose(); + disposables.length = 0; + }); } window.clearTimeout(this._linesCacheTimeoutId); @@ -682,7 +693,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA const disposables: IDisposable[] = []; disposables.push(marker); disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true))); - disposables.push(decoration.onDispose(() => dispose(disposables))); + disposables.push(decoration.onDispose(() => { + for (let i = 0; i < disposables.length; ++i) disposables[i].dispose(); + disposables.length = 0; + })); this._selectedDecoration.value = { decoration, match: result, dispose() { decoration.dispose(); } }; } } @@ -744,7 +758,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA const disposables: IDisposable[] = []; disposables.push(marker); disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false))); - disposables.push(findResultDecoration.onDispose(() => dispose(disposables))); + disposables.push(findResultDecoration.onDispose(() => { + for (let i = 0; i < disposables.length; ++i) disposables[i].dispose(); + disposables.length = 0; + })); } return findResultDecoration; } diff --git a/addons/addon-search/test/SearchAddon.test.ts b/addons/addon-search/test/SearchAddon.test.ts index 0045cf1c21..6055a200bc 100644 --- a/addons/addon-search/test/SearchAddon.test.ts +++ b/addons/addon-search/test/SearchAddon.test.ts @@ -22,7 +22,7 @@ test.describe('Search Tests', () => { await ctx.page.evaluate(` window.term.reset() window.search?.dispose(); - window.search = new SearchAddon(); + window.search = new SearchAddon(sharedExports); window.term.loadAddon(window.search); `); }); diff --git a/addons/addon-search/typings/addon-search.d.ts b/addons/addon-search/typings/addon-search.d.ts index 282004a2ae..afaa0c663f 100644 --- a/addons/addon-search/typings/addon-search.d.ts +++ b/addons/addon-search/typings/addon-search.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IEvent, ISharedExports } from '@xterm/xterm'; declare module '@xterm/addon-search' { /** @@ -95,7 +95,7 @@ declare module '@xterm/addon-search' { * Creates a new search addon. * @param options Options for the search addon. */ - constructor(options?: Partial); + constructor(sharedExports: ISharedExports, options?: Partial); /** * Activates the addon diff --git a/addons/addon-webgl/src/CharAtlasCache.ts b/addons/addon-webgl/src/CharAtlasCache.ts index ea02581b8d..c416fa2e00 100644 --- a/addons/addon-webgl/src/CharAtlasCache.ts +++ b/addons/addon-webgl/src/CharAtlasCache.ts @@ -4,7 +4,7 @@ */ import { TextureAtlas } from './TextureAtlas'; -import { ITerminalOptions, Terminal } from '@xterm/xterm'; +import { ISharedExports, ITerminalOptions, Terminal } from '@xterm/xterm'; import { ITerminal, ReadonlyColorSet } from 'browser/Types'; import { ICharAtlasConfig, ITextureAtlas } from './Types'; import { generateConfig, configEquals } from './CharAtlasUtils'; @@ -24,6 +24,7 @@ const charAtlasCache: ITextureAtlasCacheEntry[] = []; * one that is in use by another terminal. */ export function acquireTextureAtlas( + sharedExports: ISharedExports, terminal: Terminal, options: Required, colors: ReadonlyColorSet, @@ -67,7 +68,7 @@ export function acquireTextureAtlas( const core: ITerminal = (terminal as any)._core; const newEntry: ITextureAtlasCacheEntry = { - atlas: new TextureAtlas(document, newConfig, core.unicodeService), + atlas: new TextureAtlas(sharedExports, document, newConfig, core.unicodeService), config: newConfig, ownedBy: [terminal] }; diff --git a/addons/addon-webgl/src/DevicePixelObserver.ts b/addons/addon-webgl/src/DevicePixelObserver.ts index 33d733ef57..5842283747 100644 --- a/addons/addon-webgl/src/DevicePixelObserver.ts +++ b/addons/addon-webgl/src/DevicePixelObserver.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ISharedExports, IDisposable } from '@xterm/xterm'; -export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { +export function observeDevicePixelDimensions(sharedExports: ISharedExports, element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { // Observe any resizes to the element and extract the actual pixel size of the element if the // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when // converting between CSS pixels and device pixels which causes blurry rendering when device @@ -36,5 +36,5 @@ export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: observer.disconnect(); observer = undefined; } - return toDisposable(() => observer?.disconnect()); + return sharedExports.toDisposable(() => observer?.disconnect()); } diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 10c9ad7222..1237e28e28 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -5,12 +5,12 @@ import { TextureAtlas } from './TextureAtlas'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Terminal } from '@xterm/xterm'; +import { ISharedExports, Terminal } from '@xterm/xterm'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types'; import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils'; import type { IOptionsService } from 'common/services/Services'; import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; interface IVertices { attributes: Float32Array; @@ -88,7 +88,7 @@ let $glyph: IRasterizedGlyph | undefined = undefined; let $leftCellPadding = 0; let $clippedPixels = 0; -export class GlyphRenderer extends Disposable { +export class GlyphRenderer extends AddonDisposable { private readonly _program: WebGLProgram; private readonly _vertexArrayObject: IWebGLVertexArrayObject; private readonly _projectionLocation: WebGLUniformLocation; @@ -109,12 +109,13 @@ export class GlyphRenderer extends Disposable { }; constructor( + private _sharedExports: ISharedExports, private readonly _terminal: Terminal, private readonly _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions, private readonly _optionsService: IOptionsService ) { - super(); + super(_sharedExports); const gl = this._gl; @@ -126,7 +127,7 @@ export class GlyphRenderer extends Disposable { } this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages))); - this._register(toDisposable(() => gl.deleteProgram(this._program))); + this._register(_sharedExports.toDisposable(() => gl.deleteProgram(this._program))); // Uniform locations this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection')); @@ -140,7 +141,7 @@ export class GlyphRenderer extends Disposable { // Setup a_unitquad, this defines the 4 vertices of a rectangle const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); const unitQuadVerticesBuffer = gl.createBuffer(); - this._register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer))); + this._register(_sharedExports.toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer))); gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer); gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW); gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD); @@ -151,13 +152,13 @@ export class GlyphRenderer extends Disposable { // triangle strip const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]); const elementIndicesBuffer = gl.createBuffer(); - this._register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); + this._register(_sharedExports.toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); // Setup attributes this._attributesBuffer = throwIfFalsy(gl.createBuffer()); - this._register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer))); + this._register(_sharedExports.toDisposable(() => gl.deleteBuffer(this._attributesBuffer))); gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.enableVertexAttribArray(VertexAttribLocations.OFFSET); gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0); @@ -192,7 +193,7 @@ export class GlyphRenderer extends Disposable { this._atlasTextures = []; for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) { const glTexture = new GLTexture(throwIfFalsy(gl.createTexture())); - this._register(toDisposable(() => gl.deleteTexture(glTexture.texture))); + this._register(_sharedExports.toDisposable(() => gl.deleteTexture(glTexture.texture))); gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, glTexture.texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); diff --git a/addons/addon-webgl/src/RectangleRenderer.ts b/addons/addon-webgl/src/RectangleRenderer.ts index 8ace9076e9..46da70222b 100644 --- a/addons/addon-webgl/src/RectangleRenderer.ts +++ b/addons/addon-webgl/src/RectangleRenderer.ts @@ -7,13 +7,13 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { IThemeService } from 'browser/services/Services'; import { ReadonlyColorSet } from 'browser/Types'; import { Attributes, FgFlags } from 'common/buffer/Constants'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IColor } from 'common/Types'; -import { Terminal } from '@xterm/xterm'; +import { ISharedExports, Terminal } from '@xterm/xterm'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types'; import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; const enum VertexAttribLocations { POSITION = 0, @@ -73,7 +73,7 @@ let $g = 0; let $b = 0; let $a = 0; -export class RectangleRenderer extends Disposable { +export class RectangleRenderer extends AddonDisposable { private _program: WebGLProgram; private _vertexArrayObject: IWebGLVertexArrayObject; @@ -86,17 +86,18 @@ export class RectangleRenderer extends Disposable { private _verticesCursor: Vertices = new Vertices(); constructor( + private _sharedExports: ISharedExports, private _terminal: Terminal, private _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions, private readonly _themeService: IThemeService ) { - super(); + super(_sharedExports); const gl = this._gl; this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); - this._register(toDisposable(() => gl.deleteProgram(this._program))); + this._register(_sharedExports.toDisposable(() => gl.deleteProgram(this._program))); // Uniform locations this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection')); @@ -108,7 +109,7 @@ export class RectangleRenderer extends Disposable { // Setup a_unitquad, this defines the 4 vertices of a rectangle const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); const unitQuadVerticesBuffer = gl.createBuffer(); - this._register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer))); + this._register(_sharedExports.toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer))); gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer); gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW); gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD); @@ -119,13 +120,13 @@ export class RectangleRenderer extends Disposable { // triangle strip const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]); const elementIndicesBuffer = gl.createBuffer(); - this._register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); + this._register(_sharedExports.toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW); // Setup attributes this._attributesBuffer = throwIfFalsy(gl.createBuffer()); - this._register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer))); + this._register(_sharedExports.toDisposable(() => gl.deleteBuffer(this._attributesBuffer))); gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.enableVertexAttribArray(VertexAttribLocations.POSITION); gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0); diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index a7d1138ee0..0bcaa1688e 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -15,7 +15,7 @@ import { IColor } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; import { IUnicodeService } from 'common/services/Services'; -import { Emitter } from 'vs/base/common/event'; +import { ISharedExports, IEmitter, IEvent } from '@xterm/xterm'; /** * A shared object which is used to draw nothing for a particular cell. @@ -80,16 +80,22 @@ export class TextureAtlas implements ITextureAtlas { public static maxAtlasPages: number | undefined; public static maxTextureSize: number | undefined; - private readonly _onAddTextureAtlasCanvas = new Emitter(); - public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; - private readonly _onRemoveTextureAtlasCanvas = new Emitter(); - public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; + private readonly _onAddTextureAtlasCanvas: IEmitter; + public readonly onAddTextureAtlasCanvas: IEvent; + private readonly _onRemoveTextureAtlasCanvas: IEmitter; + public readonly onRemoveTextureAtlasCanvas: IEvent; constructor( + _sharedExports: ISharedExports, private readonly _document: Document, private readonly _config: ICharAtlasConfig, private readonly _unicodeService: IUnicodeService ) { + this._onAddTextureAtlasCanvas = new _sharedExports.Emitter(); + this.onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; + this._onRemoveTextureAtlasCanvas = new _sharedExports.Emitter(); + this.onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; + this._createNewPage(); this._tmpCanvas = createCanvas( _document, diff --git a/addons/addon-webgl/src/Types.ts b/addons/addon-webgl/src/Types.ts index e030e579ed..faf249f9b5 100644 --- a/addons/addon-webgl/src/Types.ts +++ b/addons/addon-webgl/src/Types.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { FontWeight } from '@xterm/xterm'; +import { FontWeight, IEvent } from '@xterm/xterm'; import { IColorSet } from 'browser/Types'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types'; -import type { Event } from 'vs/base/common/event'; + export interface IRenderModel { cells: Uint32Array; @@ -58,8 +58,8 @@ export interface ICharAtlasConfig { export interface ITextureAtlas extends IDisposable { readonly pages: { canvas: HTMLCanvasElement, version: number }[]; - onAddTextureAtlasCanvas: Event; - onRemoveTextureAtlasCanvas: Event; + onAddTextureAtlasCanvas: IEvent; + onRemoveTextureAtlasCanvas: IEvent; /** * Warm up the texture atlas, adding common glyphs to avoid slowing early frame. diff --git a/addons/addon-webgl/src/WebglAddon.ts b/addons/addon-webgl/src/WebglAddon.ts index 49a796de9a..13e748a41d 100644 --- a/addons/addon-webgl/src/WebglAddon.ts +++ b/addons/addon-webgl/src/WebglAddon.ts @@ -3,32 +3,33 @@ * @license MIT */ -import type { ITerminalAddon, Terminal } from '@xterm/xterm'; +import type { ISharedExports, ITerminalAddon, Terminal, IEmitter, IEvent } from '@xterm/xterm'; import type { WebglAddon as IWebglApi } from '@xterm/addon-webgl'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { ITerminal } from 'browser/Types'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { getSafariVersion, isSafari } from 'common/Platform'; import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; import { IWebGL2RenderingContext } from './Types'; import { WebglRenderer } from './WebglRenderer'; import { setTraceLogger } from 'common/services/LogService'; -import { Emitter, Event } from 'vs/base/common/event'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; +import { forwardEvent } from './WebglUtils'; -export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi { +export class WebglAddon extends AddonDisposable implements ITerminalAddon , IWebglApi { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private readonly _onChangeTextureAtlas = this._register(new Emitter()); - public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; - private readonly _onAddTextureAtlasCanvas = this._register(new Emitter()); - public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; - private readonly _onRemoveTextureAtlasCanvas = this._register(new Emitter()); - public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; - private readonly _onContextLoss = this._register(new Emitter()); - public readonly onContextLoss = this._onContextLoss.event; + private readonly _onChangeTextureAtlas: IEmitter; + public readonly onChangeTextureAtlas: IEvent; + private readonly _onAddTextureAtlasCanvas: IEmitter; + public readonly onAddTextureAtlasCanvas: IEvent; + private readonly _onRemoveTextureAtlasCanvas: IEmitter; + public readonly onRemoveTextureAtlasCanvas: IEvent; + private readonly _onContextLoss: IEmitter; + public readonly onContextLoss: IEvent; constructor( + private _sharedExports: ISharedExports, private _preserveDrawingBuffer?: boolean ) { if (isSafari && getSafariVersion() < 16) { @@ -43,7 +44,16 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi throw new Error('Webgl2 is only supported on Safari 16 and above'); } } - super(); + super(_sharedExports); + + this._onChangeTextureAtlas = this._register(new _sharedExports.Emitter()); + this.onChangeTextureAtlas = this._onChangeTextureAtlas.event; + this._onAddTextureAtlasCanvas = this._register(new _sharedExports.Emitter()); + this.onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; + this._onRemoveTextureAtlasCanvas = this._register(new _sharedExports.Emitter()); + this.onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; + this._onContextLoss = this._register(new _sharedExports.Emitter()); + this.onContextLoss = this._onContextLoss.event; } public activate(terminal: Terminal): void { @@ -71,6 +81,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi setTraceLogger(logService); this._renderer = this._register(new WebglRenderer( + this._sharedExports, terminal, characterJoinerService, charSizeService, @@ -81,13 +92,13 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi themeService, this._preserveDrawingBuffer )); - this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss)); - this._register(Event.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); - this._register(Event.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); - this._register(Event.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)); + this._register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); + this._register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + this._register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); + this._register(forwardEvent(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)); renderService.setRenderer(this._renderer); - this._register(toDisposable(() => { + this._register(this._sharedExports.toDisposable(() => { if ((this._terminal as any)._core._store._isDisposed) { return; } diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 398594d942..6c73635a53 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -15,26 +15,25 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { Terminal } from '@xterm/xterm'; +import { ISharedExports, Terminal, IMutableDisposable, IDisposable, IEmitter, IEvent } from '@xterm/xterm'; import { GlyphRenderer } from './GlyphRenderer'; import { RectangleRenderer } from './RectangleRenderer'; import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel'; import { IWebGL2RenderingContext, type ITextureAtlas } from './Types'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { IRenderLayer } from './renderLayer/Types'; -import { Emitter, Event } from 'vs/base/common/event'; -import { addDisposableListener } from 'vs/base/browser/dom'; -import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; +import { forwardEvent } from './WebglUtils'; -export class WebglRenderer extends Disposable implements IRenderer { +export class WebglRenderer extends AddonDisposable implements IRenderer { private _renderLayers: IRenderLayer[]; - private _cursorBlinkStateManager: MutableDisposable = new MutableDisposable(); - private _charAtlasDisposable = this._register(new MutableDisposable()); + private _cursorBlinkStateManager: IMutableDisposable; + private _charAtlasDisposable: IMutableDisposable; private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; private _deviceMaxTextureSize: number; - private _observerDisposable = this._register(new MutableDisposable()); + private _observerDisposable: IMutableDisposable; private _model: RenderModel = new RenderModel(); private _workCell: ICellData = new CellData(); @@ -43,8 +42,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; - private _rectangleRenderer: MutableDisposable = this._register(new MutableDisposable()); - private _glyphRenderer: MutableDisposable = this._register(new MutableDisposable()); + private _rectangleRenderer: IMutableDisposable; + private _glyphRenderer: IMutableDisposable; public readonly dimensions: IRenderDimensions; @@ -52,18 +51,19 @@ export class WebglRenderer extends Disposable implements IRenderer { private _isAttached: boolean; private _contextRestorationTimeout: number | undefined; - private readonly _onChangeTextureAtlas = this._register(new Emitter()); - public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; - private readonly _onAddTextureAtlasCanvas = this._register(new Emitter()); - public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; - private readonly _onRemoveTextureAtlasCanvas = this._register(new Emitter()); - public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; - private readonly _onRequestRedraw = this._register(new Emitter()); - public readonly onRequestRedraw = this._onRequestRedraw.event; - private readonly _onContextLoss = this._register(new Emitter()); - public readonly onContextLoss = this._onContextLoss.event; + private readonly _onChangeTextureAtlas: IEmitter; + public readonly onChangeTextureAtlas: IEvent; + private readonly _onAddTextureAtlasCanvas: IEmitter; + public readonly onAddTextureAtlasCanvas: IEvent; + private readonly _onRemoveTextureAtlasCanvas: IEmitter; + public readonly onRemoveTextureAtlasCanvas: IEvent; + private readonly _onRequestRedraw: IEmitter; + public readonly onRequestRedraw: IEvent; + private readonly _onContextLoss: IEmitter; + public readonly onContextLoss: IEvent; constructor( + private _sharedExports: ISharedExports, private _terminal: Terminal, private readonly _characterJoinerService: ICharacterJoinerService, private readonly _charSizeService: ICharSizeService, @@ -74,7 +74,24 @@ export class WebglRenderer extends Disposable implements IRenderer { private readonly _themeService: IThemeService, preserveDrawingBuffer?: boolean ) { - super(); + super(_sharedExports); + + this._cursorBlinkStateManager = new _sharedExports.MutableDisposable(); + this._charAtlasDisposable = this._register(new _sharedExports.MutableDisposable()); + this._observerDisposable = this._register(new _sharedExports.MutableDisposable()); + this._rectangleRenderer = this._register(new _sharedExports.MutableDisposable()); + this._glyphRenderer = this._register(new _sharedExports.MutableDisposable()); + + this._onChangeTextureAtlas = this._register(new _sharedExports.Emitter()); + this.onChangeTextureAtlas = this._onChangeTextureAtlas.event; + this._onAddTextureAtlasCanvas = this._register(new _sharedExports.Emitter()); + this.onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; + this._onRemoveTextureAtlasCanvas = this._register(new _sharedExports.Emitter()); + this.onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; + this._onRequestRedraw = this._register(new _sharedExports.Emitter()); + this.onRequestRedraw = this._onRequestRedraw.event; + this._onContextLoss = this._register(new _sharedExports.Emitter()); + this.onContextLoss = this._onContextLoss.event; this._register(this._themeService.onChangeColors(() => this._handleColorChange())); @@ -83,7 +100,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier!, this._coreBrowserService, _optionsService, this._themeService) + new LinkRenderLayer(_sharedExports, this._core.screenElement!, 2, this._terminal, this._core.linkifier!, this._coreBrowserService, _optionsService, this._themeService) ]; this.dimensions = createRenderDimensions(); this._devicePixelRatio = this._coreBrowserService.dpr; @@ -128,9 +145,9 @@ export class WebglRenderer extends Disposable implements IRenderer { this._requestRedrawViewport(); })); - this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this._observerDisposable.value = observeDevicePixelDimensions(this._sharedExports, this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); this._register(this._coreBrowserService.onWindowChange(w => { - this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this._observerDisposable.value = observeDevicePixelDimensions(this._sharedExports, this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); })); this._core.screenElement!.appendChild(this._canvas); @@ -139,7 +156,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!); - this._register(toDisposable(() => { + this._register(_sharedExports.toDisposable(() => { for (const l of this._renderLayers) { l.dispose(); } @@ -248,8 +265,8 @@ export class WebglRenderer extends Disposable implements IRenderer { * Initializes members dependent on WebGL context state. */ private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] { - this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService); - this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions, this._optionsService); + this._rectangleRenderer.value = new RectangleRenderer(this._sharedExports, this._terminal, this._gl, this.dimensions, this._themeService); + this._glyphRenderer.value = new GlyphRenderer(this._sharedExports, this._terminal, this._gl, this.dimensions, this._optionsService); // Update dimensions and acquire char atlas this.handleCharSizeChanged(); @@ -268,6 +285,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } const atlas = acquireTextureAtlas( + this._sharedExports, this._terminal, this._optionsService.rawOptions, this._themeService.colors, @@ -280,10 +298,14 @@ export class WebglRenderer extends Disposable implements IRenderer { ); if (this._charAtlas !== atlas) { this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); - this._charAtlasDisposable.value = combinedDisposable( - Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas), - Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas) - ); + const disposables = [ + forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas), + forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas) + ]; + this._charAtlasDisposable.value = this._sharedExports.toDisposable(() => { + for (let i = 0; i < disposables.length; ++i) disposables[i].dispose(); + disposables.length = 0; + }); } this._charAtlas = atlas; this._charAtlas.warmUp(); @@ -675,3 +697,42 @@ export class JoinedCellData extends AttributeData implements ICellData { function clamp(value: number, max: number, min: number = 0): number { return Math.max(Math.min(value, max), min); } + + + + +/** borrowed from vs */ +class DomListener implements IDisposable { + private _handler: (e: any) => void; + private _node: EventTarget; + private readonly _type: string; + private readonly _options: boolean | AddEventListenerOptions; + + constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) { + this._node = node; + this._type = type; + this._handler = handler; + this._options = (options || false); + this._node.addEventListener(this._type, this._handler, this._options); + } + + public dispose(): void { + if (!this._handler) { + // Already disposed + return; + } + + this._node.removeEventListener(this._type, this._handler, this._options); + + // Prevent leakers from holding on to the dom or handler func + this._node = null!; + this._handler = null!; + } +} + +export function addDisposableListener(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable; +export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable; +export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable; +export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable { + return new DomListener(node, type, handler, useCaptureOrOptions); +} diff --git a/addons/addon-webgl/src/WebglUtils.ts b/addons/addon-webgl/src/WebglUtils.ts index da765c8020..f721f9414a 100644 --- a/addons/addon-webgl/src/WebglUtils.ts +++ b/addons/addon-webgl/src/WebglUtils.ts @@ -3,6 +3,7 @@ * @license MIT */ +import { IDisposable, IEmitter, IEvent } from '@xterm/xterm'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; /** @@ -61,3 +62,7 @@ export class GLTexture { this.version = -1; } } + +export function forwardEvent(from: IEvent, to: IEmitter): IDisposable { + return from(e => to.fire(e)); +} diff --git a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts index 85b170e76c..35cb746551 100644 --- a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -7,16 +7,17 @@ import { ReadonlyColorSet } from 'browser/Types'; import { acquireTextureAtlas } from '../CharAtlasCache'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; -import { Terminal } from '@xterm/xterm'; +import { ISharedExports, Terminal } from '@xterm/xterm'; import { IRenderLayer } from './Types'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { TEXT_BASELINE } from '../Constants'; import type { ITextureAtlas } from '../Types'; +import { AddonDisposable } from 'common/shared/AddonDisposable'; -export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { + +export abstract class BaseRenderLayer extends AddonDisposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; private _deviceCharWidth: number = 0; @@ -29,6 +30,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _charAtlas: ITextureAtlas | undefined; constructor( + private _sharedExports: ISharedExports, terminal: Terminal, private _container: HTMLElement, id: string, @@ -38,7 +40,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected readonly _optionsService: IOptionsService, protected readonly _themeService: IThemeService ) { - super(); + super(_sharedExports); this._canvas = this._coreBrowserService.mainDocument.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); @@ -48,7 +50,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._refreshCharAtlas(terminal, e); this.reset(terminal); })); - this._register(toDisposable(() => { + this._register(_sharedExports.toDisposable(() => { this._canvas.remove(); })); } @@ -96,7 +98,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer return; } - this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048); + this._charAtlas = acquireTextureAtlas(this._sharedExports, terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048); this._charAtlas.warmUp(); } diff --git a/addons/addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/addon-webgl/src/renderLayer/LinkRenderLayer.ts index faceac44fa..3a97f85756 100644 --- a/addons/addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -9,13 +9,14 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ILinkifier2, ILinkifierEvent } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; -import { Terminal } from '@xterm/xterm'; +import { ISharedExports, Terminal } from '@xterm/xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; constructor( + sharedExports: ISharedExports, container: HTMLElement, zIndex: number, terminal: Terminal, @@ -24,7 +25,7 @@ export class LinkRenderLayer extends BaseRenderLayer { optionsService: IOptionsService, themeService: IThemeService ) { - super(terminal, container, 'link', zIndex, true, coreBrowserService, optionsService, themeService); + super(sharedExports, terminal, container, 'link', zIndex, true, coreBrowserService, optionsService, themeService); this._register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e))); this._register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e))); diff --git a/addons/addon-webgl/test/WebglRenderer.test.ts b/addons/addon-webgl/test/WebglRenderer.test.ts index af61992ced..f414c697e1 100644 --- a/addons/addon-webgl/test/WebglRenderer.test.ts +++ b/addons/addon-webgl/test/WebglRenderer.test.ts @@ -15,7 +15,7 @@ test.beforeAll(async ({ browser }) => { await openTerminal(ctx); ctxWrapper.value = ctx; await ctx.page.evaluate(` - window.addon = new window.WebglAddon(true); + window.addon = new window.WebglAddon(sharedExports, true); window.term.loadAddon(window.addon); `); }); @@ -31,7 +31,7 @@ test.describe('WebGL Renderer Integration Tests', async () => { injectSharedRendererTests(ctxWrapper); injectSharedRendererTestsStandalone(ctxWrapper, async () => { await ctx.page.evaluate(` - window.addon = new window.WebglAddon(true); + window.addon = new window.WebglAddon(sharedExports, true); window.term.loadAddon(window.addon); `); }); diff --git a/addons/addon-webgl/typings/addon-webgl.d.ts b/addons/addon-webgl/typings/addon-webgl.d.ts index 1d7e1510c7..d9940af30b 100644 --- a/addons/addon-webgl/typings/addon-webgl.d.ts +++ b/addons/addon-webgl/typings/addon-webgl.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IEvent, ISharedExports } from '@xterm/xterm'; declare module '@xterm/addon-webgl' { /** @@ -32,7 +32,7 @@ declare module '@xterm/addon-webgl' { */ public readonly onRemoveTextureAtlasCanvas: IEvent; - constructor(preserveDrawingBuffer?: boolean); + constructor(sharedExports: ISharedExports, preserveDrawingBuffer?: boolean); /** * Activates the addon. diff --git a/demo/client.ts b/demo/client.ts index f5903aa4d7..9edcf0c152 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -16,7 +16,7 @@ if ('WebAssembly' in window) { ImageAddon = imageAddon.ImageAddon; } -import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm'; +import { Terminal, ITerminalOptions, type IDisposable, type ITheme, sharedExports, ISharedExports } from '@xterm/xterm'; import { AttachAddon } from '@xterm/addon-attach'; import { ClipboardAddon } from '@xterm/addon-clipboard'; import { FitAddon } from '@xterm/addon-fit'; @@ -32,6 +32,7 @@ import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; export interface IWindowWithTerminal extends Window { term: typeof Terminal; Terminal: typeof Terminal; + sharedExports: ISharedExports; AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention ClipboardAddon?: typeof ClipboardAddon; // eslint-disable-line @typescript-eslint/naming-convention FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention @@ -214,6 +215,7 @@ const createNewWindowButtonHandler: () => void = () => { if (document.location.pathname === '/test') { window.Terminal = Terminal; + window.sharedExports = sharedExports; window.AttachAddon = AttachAddon; window.ClipboardAddon = ClipboardAddon; window.FitAddon = FitAddon; @@ -275,15 +277,15 @@ function createTerminal(): void { // Load addons const typedTerm = term as Terminal; - addons.search.instance = new SearchAddon(); + addons.search.instance = new SearchAddon(sharedExports); addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); - addons.image.instance = new ImageAddon(); - addons.progress.instance = new ProgressAddon(); + addons.image.instance = new ImageAddon(sharedExports); + addons.progress.instance = new ProgressAddon(sharedExports); addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon(); addons.clipboard.instance = new ClipboardAddon(); try { // try to start with webgl renderer (might throw on older safari/webkit) - addons.webgl.instance = new WebglAddon(); + addons.webgl.instance = new WebglAddon(sharedExports); } catch (e) { console.warn(e); } @@ -637,8 +639,8 @@ function initAddons(term: Terminal): void { if (checkbox.checked) { const ctorOptionsJson = document.querySelector('#image-options').value; addon.instance = ctorOptionsJson - ? new addons[name].ctor(JSON.parse(ctorOptionsJson)) - : new addons[name].ctor(); + ? new addons[name].ctor(sharedExports, JSON.parse(ctorOptionsJson)) + : new addons[name].ctor(sharedExports); term.loadAddon(addon.instance); } else { addon.instance!.dispose(); @@ -660,7 +662,8 @@ function initAddons(term: Terminal): void { } if (checkbox.checked) { // HACK: Manually remove addons that cannot be changes - addon.instance = new (addon as IDemoAddon>).ctor(); + // FIXME: re-enable this once done with the sharedExports + //addon.instance = new (addon as IDemoAddon>).ctor(); try { term.loadAddon(addon.instance); if (name === 'webgl') { @@ -693,7 +696,7 @@ function initAddons(term: Terminal): void { if (addons.webgl.instance) { preDisposeWebgl(); addons.webgl.instance.dispose(); - addons.webgl.instance = new addons.webgl.ctor(); + addons.webgl.instance = new addons.webgl.ctor(sharedExports); term.loadAddon(addons.webgl.instance); postInitWebgl(); } diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 3b4309437f..2f895fb44b 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -6,14 +6,27 @@ import * as Strings from 'browser/LocalizableStrings'; import { CoreBrowserTerminal as TerminalCore } from 'browser/CoreBrowserTerminal'; import { IBufferRange, ITerminal } from 'browser/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ITerminalOptions } from 'common/Types'; import { AddonManager } from 'common/public/AddonManager'; import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; import { ParserApi } from 'common/public/ParserApi'; import { UnicodeApi } from 'common/public/UnicodeApi'; -import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm'; -import type { Event } from 'vs/base/common/event'; +import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling, ISharedExports } from '@xterm/xterm'; +import { type Event, Emitter } from 'vs/base/common/event'; + + +/** + * EXPERIMENTAL: + * Expose certain building blocks on the module to be used at runtime in addons. + */ +export const sharedExports: ISharedExports = { + DisposableStore, + MutableDisposable, + Emitter, + toDisposable +}; + /** * The set of options that only have an effect when set in the Terminal constructor. diff --git a/src/common/shared/AddonDisposable.ts b/src/common/shared/AddonDisposable.ts new file mode 100644 index 0000000000..f41d526d7e --- /dev/null +++ b/src/common/shared/AddonDisposable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable, IDisposableStore, ISharedExports } from '@xterm/xterm'; + + +export class AddonDisposable implements IDisposable { + protected readonly _store: IDisposableStore; + + constructor(sharedExports: ISharedExports) { + this._store = new sharedExports.DisposableStore(); + } + + protected _register(o: T): T { + if ((o as unknown as IDisposable) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + return this._store.add(o); + } + + public dispose(): void { + this._store.dispose(); + } +} diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 738570c9e7..6ab7f2f290 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -6,12 +6,25 @@ import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; import { ParserApi } from 'common/public/ParserApi'; import { UnicodeApi } from 'common/public/UnicodeApi'; -import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi } from '@xterm/headless'; +import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi, ISharedExports } from '@xterm/headless'; import { Terminal as TerminalCore } from 'headless/Terminal'; import { AddonManager } from 'common/public/AddonManager'; import { ITerminalOptions } from 'common/Types'; -import { Disposable } from 'vs/base/common/lifecycle'; -import type { Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { type Event, Emitter } from 'vs/base/common/event'; + + +/** + * EXPERIMENTAL: + * Expose certain building blocks on the module to be used at runtime in addons. + */ +export const sharedExports: ISharedExports = { + DisposableStore, + Emitter, + toDisposable +}; + + /** * The set of options that only have an effect when set in the Terminal constructor. */ diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 1085db9b43..2c7b8b88bf 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -1358,4 +1358,31 @@ declare module '@xterm/headless' { */ readonly wraparoundMode: boolean; } + + /** + * EXPERIMENTAL: + * Module exposure of certain building blocks to be used at runtime in addons. + */ + export interface ISharedExports { + readonly DisposableStore: new() => IDisposableStore; + readonly Emitter: new() => IEmitter; + readonly toDisposable: (fn: () => void) => IDisposable; + } + + export const sharedExports: ISharedExports; + + // FIXME: @Tyriar - plz have a look at the following interfaces and + // to what degree those should be exposed or get stripped down + export interface IEmitter extends IDisposable { + event: IEvent; + fire(event: T): void; + hasListeners(): boolean; + } + export interface IDisposableStore extends IDisposable { + isDisposed: boolean; + clear(): void; + add(o: T): T; + delete(o: T): void; + deleteAndLeak(o: T): void; + } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 15a0332789..d67cf385ba 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1956,4 +1956,37 @@ declare module '@xterm/xterm' { */ readonly wraparoundMode: boolean; } + + /** + * EXPERIMENTAL: + * Module exposure of certain building blocks to be used at runtime in addons. + */ + export interface ISharedExports { + readonly DisposableStore: new() => IDisposableStore; + readonly MutableDisposable: new() => IMutableDisposable; + readonly Emitter: new() => IEmitter; + readonly toDisposable: (fn: () => void) => IDisposable; + } + + export const sharedExports: ISharedExports; + + // FIXME: @Tyriar - plz have a look at the following interfaces and + // to what degree those should be exposed or get stripped down + export interface IEmitter extends IDisposable { + event: IEvent; + fire(event: T): void; + hasListeners(): boolean; + } + export interface IDisposableStore extends IDisposable { + isDisposed: boolean; + clear(): void; + add(o: T): T; + delete(o: T): void; + deleteAndLeak(o: T): void; + } + export interface IMutableDisposable extends IDisposable { + value: T | undefined; + clear(): void; + dispose(): void; + } }