diff --git a/addons/addon-image/src/ImageRenderer.ts b/addons/addon-image/src/ImageRenderer.ts index 1e87aa97..1068c467 100644 --- a/addons/addon-image/src/ImageRenderer.ts +++ b/addons/addon-image/src/ImageRenderer.ts @@ -6,7 +6,7 @@ import { toRGBA8888 } from 'sixel/lib/Colors'; import { IDisposable } from '@xterm/xterm'; import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types'; -import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; const PLACEHOLDER_LENGTH = 4096; const PLACEHOLDER_HEIGHT = 24; diff --git a/addons/addon-progress/src/ProgressAddon.ts b/addons/addon-progress/src/ProgressAddon.ts index 175d7af0..9d71179e 100644 --- a/addons/addon-progress/src/ProgressAddon.ts +++ b/addons/addon-progress/src/ProgressAddon.ts @@ -5,7 +5,7 @@ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress'; -import type { Emitter, Event } from 'vs/base/common/event'; +import type { Emitter, IEvent } from 'common/Event'; const enum ProgressType { @@ -39,7 +39,7 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi { 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; + public onChange!: IEvent; public dispose(): void { this._seqHandler?.dispose(); diff --git a/addons/addon-progress/src/tsconfig.json b/addons/addon-progress/src/tsconfig.json index 8c9bc2fe..93e9eeb2 100644 --- a/addons/addon-progress/src/tsconfig.json +++ b/addons/addon-progress/src/tsconfig.json @@ -15,6 +15,9 @@ "../../../node_modules/@types/mocha" ], "paths": { + "common/*": [ + "../../../src/common/*" + ], "browser/*": [ "../../../src/browser/*" ], @@ -34,6 +37,9 @@ { "path": "../../../src/browser" }, + { + "path": "../../../src/common" + }, { "path": "../../../src/vs" } diff --git a/addons/addon-search/src/DecorationManager.ts b/addons/addon-search/src/DecorationManager.ts index f297b69f..a8ec1201 100644 --- a/addons/addon-search/src/DecorationManager.ts +++ b/addons/addon-search/src/DecorationManager.ts @@ -5,7 +5,7 @@ import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm'; import type { ISearchDecorationOptions } from '@xterm/addon-search'; -import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { dispose, Disposable, toDisposable } from 'common/Lifecycle'; import type { ISearchResult } from './SearchEngine'; /** diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index ffbe9401..bf892b78 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -5,9 +5,9 @@ import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm'; import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { disposableTimeout } from 'vs/base/common/async'; +import { Emitter, type IEvent } from 'common/Event'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; +import { disposableTimeout } from 'common/Async'; import { SearchLineCache } from './SearchLineCache'; import { SearchState } from './SearchState'; import { SearchEngine, type ISearchResult } from './SearchEngine'; @@ -47,7 +47,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp private readonly _onBeforeSearch = this._register(new Emitter()); public readonly onBeforeSearch = this._onBeforeSearch.event; - public get onDidChangeResults(): Event { + public get onDidChangeResults(): IEvent { return this._resultTracker.onDidChangeResults; } diff --git a/addons/addon-search/src/SearchEngine.test.ts b/addons/addon-search/src/SearchEngine.test.ts index dfdef125..b3cea8f5 100644 --- a/addons/addon-search/src/SearchEngine.test.ts +++ b/addons/addon-search/src/SearchEngine.test.ts @@ -7,7 +7,7 @@ import { SearchEngine } from './SearchEngine'; import { SearchLineCache } from './SearchLineCache'; import { Terminal } from 'browser/public/Terminal'; import type { ISearchOptions } from '@xterm/addon-search'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'common/Lifecycle'; function writeP(terminal: Terminal, data: string): Promise { return new Promise(r => terminal.write(data, r)); diff --git a/addons/addon-search/src/SearchLineCache.test.ts b/addons/addon-search/src/SearchLineCache.test.ts index 3e0ec362..77f64c81 100644 --- a/addons/addon-search/src/SearchLineCache.test.ts +++ b/addons/addon-search/src/SearchLineCache.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { SearchLineCache, LineCacheEntry } from './SearchLineCache'; import { Terminal } from 'browser/public/Terminal'; -import { timeout } from 'vs/base/common/async'; +import { timeout } from 'common/Async'; function writeP(terminal: Terminal, data: string): Promise { return new Promise(r => terminal.write(data, r)); diff --git a/addons/addon-search/src/SearchLineCache.ts b/addons/addon-search/src/SearchLineCache.ts index 175f3919..526f4bfc 100644 --- a/addons/addon-search/src/SearchLineCache.ts +++ b/addons/addon-search/src/SearchLineCache.ts @@ -4,8 +4,8 @@ */ import type { Terminal } from '@xterm/xterm'; -import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { disposableTimeout } from 'vs/base/common/async'; +import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; +import { disposableTimeout } from 'common/Async'; export type LineCacheEntry = [ /** diff --git a/addons/addon-search/src/SearchResultTracker.ts b/addons/addon-search/src/SearchResultTracker.ts index f81d5291..285345b7 100644 --- a/addons/addon-search/src/SearchResultTracker.ts +++ b/addons/addon-search/src/SearchResultTracker.ts @@ -5,8 +5,8 @@ import type { ISearchResultChangeEvent } from '@xterm/addon-search'; import type { IDisposable } from '@xterm/xterm'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, type IEvent } from 'common/Event'; +import { Disposable } from 'common/Lifecycle'; import type { ISearchResult } from './SearchEngine'; /** @@ -25,7 +25,7 @@ export class SearchResultTracker extends Disposable { private _selectedDecoration: ISelectedDecoration | undefined; private readonly _onDidChangeResults = this._register(new Emitter()); - public get onDidChangeResults(): Event { return this._onDidChangeResults.event; } + public get onDidChangeResults(): IEvent { return this._onDidChangeResults.event; } /** * Gets the current search results. diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 05af6527..bc84bded 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -23,7 +23,7 @@ import { IWebGL2RenderingContext, type ITextureAtlas } from './Types'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { IRenderLayer } from './renderLayer/Types'; import { Emitter, EventUtils } from 'common/Event'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener } from 'browser/Dom'; import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index e202b819..1d3c44ae 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -10,7 +10,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle'; import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IInstantiationService } from 'common/services/Services'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener } from 'browser/Dom'; const MAX_ROWS_TO_READ = 20; diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 51d40224..1e6ae5c4 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -56,7 +56,7 @@ import { WindowsOptionsReportType } from '../common/InputHandler'; import { AccessibilityManager } from './AccessibilityManager'; import { Linkifier } from './Linkifier'; import { Emitter, EventUtils, type IEvent } from 'common/Event'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener } from 'browser/Dom'; import { MutableDisposable, toDisposable } from 'common/Lifecycle'; export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { diff --git a/src/browser/Dom.ts b/src/browser/Dom.ts new file mode 100644 index 00000000..8a43e2b0 --- /dev/null +++ b/src/browser/Dom.ts @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + * + * Minimal DOM helpers for xterm.js browser code. + */ + +import { IDisposable } from 'common/Lifecycle'; + +export function getWindow(e: Node | UIEvent | undefined | null): Window { + const candidateNode = e as Node | undefined | null; + if (candidateNode?.ownerDocument?.defaultView) { + return candidateNode.ownerDocument.defaultView; + } + + const candidateEvent = e as UIEvent | undefined | null; + if (candidateEvent?.view) { + return candidateEvent.view; + } + + return window; +} + +class DomListener implements IDisposable { + private _handler: ((e: any) => void) | null; + private _node: EventTarget | null; + private readonly _type: string; + private readonly _options: boolean | AddEventListenerOptions | undefined; + + constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) { + this._node = node; + this._type = type; + this._handler = handler; + this._options = options; + node.addEventListener(type, handler, options); + } + + public dispose(): void { + if (!this._node || !this._handler) { + return; + } + this._node.removeEventListener(this._type, this._handler, this._options); + 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); +} + +class AnimationFrameQueueItem implements IDisposable { + private _canceled = false; + + constructor(private readonly _runner: () => void, public priority: number) { + } + + public dispose(): void { + this._canceled = true; + } + + public execute(): void { + if (this._canceled) { + return; + } + try { + this._runner(); + } catch (e) { + console.error(e); + } + } + + public static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number { + return b.priority - a.priority; + } +} + +interface IWindowAnimationFrameState { + next: AnimationFrameQueueItem[]; + current: AnimationFrameQueueItem[]; + animFrameRequested: boolean; + inAnimationFrameRunner: boolean; +} + +const animationFrameState = new Map(); + +function getAnimationFrameState(targetWindow: Window): IWindowAnimationFrameState { + let state = animationFrameState.get(targetWindow); + if (!state) { + state = { + next: [], + current: [], + animFrameRequested: false, + inAnimationFrameRunner: false + }; + animationFrameState.set(targetWindow, state); + } + return state; +} + +function animationFrameRunner(targetWindow: Window): void { + const state = getAnimationFrameState(targetWindow); + state.animFrameRequested = false; + + state.current = state.next; + state.next = []; + + state.inAnimationFrameRunner = true; + while (state.current.length > 0) { + state.current.sort(AnimationFrameQueueItem.sort); + const top = state.current.shift()!; + top.execute(); + } + state.inAnimationFrameRunner = false; +} + +export function scheduleAtNextAnimationFrame(targetWindow: Window, runner: () => void, priority: number = 0): IDisposable { + const state = getAnimationFrameState(targetWindow); + const item = new AnimationFrameQueueItem(runner, priority); + state.next.push(item); + + if (!state.animFrameRequested) { + state.animFrameRequested = true; + targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindow)); + } + + return item; +} diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index d579b7fe..5e43782d 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -9,7 +9,7 @@ import { IDisposable } from 'common/Types'; import { IBufferService } from 'common/services/Services'; import { ILinkProviderService, IMouseService, IRenderService } from './services/Services'; import { Emitter } from 'common/Event'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener } from 'browser/Dom'; export class Linkifier extends Disposable implements ILinkifier2 { public get currentLink(): ILinkWithState | undefined { return this._currentLink; } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 2e1d8861..b33d198e 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -8,7 +8,7 @@ import { ViewportConstants } from 'browser/shared/Constants'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services'; import { CoreMouseEventType } from 'common/Types'; -import { addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; import { Emitter, EventUtils } from 'common/Event'; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index c564ca4a..f08eafa6 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -15,7 +15,7 @@ import { color } from 'common/Color'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services'; import { Emitter } from 'common/Event'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener } from 'browser/Dom'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts index 4e5d8b71..5052de6d 100644 --- a/src/browser/services/CoreBrowserService.ts +++ b/src/browser/services/CoreBrowserService.ts @@ -5,7 +5,7 @@ import { ICoreBrowserService } from './Services'; import { Emitter, EventUtils } from 'common/Event'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener } from 'browser/Dom'; import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; export class CoreBrowserService extends Disposable implements ICoreBrowserService { diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index e9b4e4c7..3e382575 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { getWindow } from 'vs/base/browser/dom'; +import { getWindow } from 'browser/Dom'; import { ICharSizeService, IRenderService, IMouseService } from './Services'; import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse'; diff --git a/src/common/Async.ts b/src/common/Async.ts new file mode 100644 index 00000000..f98c4942 --- /dev/null +++ b/src/common/Async.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + * + * Minimal async helpers for xterm.js core. + */ + +import { DisposableStore, IDisposable, toDisposable } from 'common/Lifecycle'; + +export function timeout(millis: number): Promise { + return new Promise(resolve => setTimeout(resolve, millis)); +} + +/** + * Creates a timeout that can be disposed using its returned value. + * @param handler The timeout handler. + * @param timeout An optional timeout in milliseconds. + * @param store An optional {@link DisposableStore} that will have the timeout disposable managed + * automatically. + */ +export function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable { + const timer = setTimeout(() => { + handler(); + if (store) { + disposable.dispose(); + } + }, timeout); + const disposable = toDisposable(() => { + clearTimeout(timer); + }); + store?.add(disposable); + return disposable; +}