diff --git a/demo/index.html b/demo/index.html index f374d998..fff77d09 100644 --- a/demo/index.html +++ b/demo/index.html @@ -32,6 +32,7 @@

Attention: The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.

+ diff --git a/demo/main.js b/demo/main.js index 1ed6a180..cccf5a04 100644 --- a/demo/main.js +++ b/demo/main.js @@ -33,23 +33,23 @@ function setPadding() { term.fit(); } -paddingElement.addEventListener('change', setPadding); +createTerminal(); -actionElements.findNext.addEventListener('keypress', function (e) { +addDomListener(paddingElement, 'change', setPadding); + +addDomListener(actionElements.findNext, 'keypress', function (e) { if (e.key === "Enter") { e.preventDefault(); term.findNext(actionElements.findNext.value); } }); -actionElements.findPrevious.addEventListener('keypress', function (e) { +addDomListener(actionElements.findPrevious, 'keypress', function (e) { if (e.key === "Enter") { e.preventDefault(); term.findPrevious(actionElements.findPrevious.value); } }); -createTerminal(); - function createTerminal() { // Clean terminal while (terminalContainer.children.length) { @@ -76,6 +76,15 @@ function createTerminal() { term.fit(); term.focus(); + const buttonHandler = () => { + term.dispose(); + term = null; + window.term = null; + socket = null; + document.getElementById('dispose').removeEventListener('click', buttonHandler); + }; + document.getElementById('dispose').addEventListener('click', buttonHandler); + // fit is called within a setTimeout, cols and rows need this. setTimeout(function () { initOptions(term); @@ -124,7 +133,7 @@ function runFakeTerminal() { term.writeln(''); term.prompt(); - term.on('key', function (key, ev) { + term._core.register(term.addDisposableListener('key', function (key, ev) { var printable = ( !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey ); @@ -139,11 +148,11 @@ function runFakeTerminal() { } else if (printable) { term.write(key); } - }); + })); - term.on('paste', function (data, ev) { + term._core.register(term.addDisposableListener('paste', function (data, ev) { term.write(data); - }); + })); } function initOptions(term) { @@ -213,14 +222,14 @@ function initOptions(term) { // Attach listeners booleanOptions.forEach(o => { var input = document.getElementById(`opt-${o}`); - input.addEventListener('change', () => { + addDomListener(input, 'change', () => { console.log('change', o, input.checked); term.setOption(o, input.checked); }); }); numberOptions.forEach(o => { var input = document.getElementById(`opt-${o}`); - input.addEventListener('change', () => { + addDomListener(input, 'change', () => { console.log('change', o, input.value); if (o === 'cols' || o === 'rows') { updateTerminalSize(); @@ -231,13 +240,18 @@ function initOptions(term) { }); Object.keys(stringOptions).forEach(o => { var input = document.getElementById(`opt-${o}`); - input.addEventListener('change', () => { + addDomListener(input, 'change', () => { console.log('change', o, input.value); term.setOption(o, input.value); }); }); } +function addDomListener(element, type, handler) { + element.addEventListener(type, handler); + term._core.register({ dispose: () => element.removeEventListener(type, handler) }); +} + function updateTerminalSize() { var cols = parseInt(document.getElementById(`opt-cols`).value, 10); var rows = parseInt(document.getElementById(`opt-rows`).value, 10); diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index ea300767..0975e0b6 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -6,9 +6,9 @@ import * as Strings from './Strings'; import { ITerminal, IBuffer } from './Types'; import { isMac } from './shared/utils/Browser'; -import { RenderDebouncer } from './utils/RenderDebouncer'; -import { addDisposableListener } from './utils/Dom'; -import { IDisposable } from 'xterm'; +import { RenderDebouncer } from './ui/RenderDebouncer'; +import { addDisposableDomListener } from './ui/Lifecycle'; +import { Disposable } from './common/Lifecycle'; const MAX_ROWS_TO_READ = 20; @@ -17,7 +17,7 @@ const enum BoundaryPosition { BOTTOM } -export class AccessibilityManager implements IDisposable { +export class AccessibilityManager extends Disposable { private _accessibilityTreeRoot: HTMLElement; private _rowContainer: HTMLElement; private _rowElements: HTMLElement[]; @@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable { private _topBoundaryFocusListener: (e: FocusEvent) => void; private _bottomBoundaryFocusListener: (e: FocusEvent) => void; - private _disposables: IDisposable[] = []; - /** * This queue has a character pushed to it for keys that are pressed, if the * next character added to the terminal is equal to the key char then it is @@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable { private _charsToConsume: string[] = []; constructor(private _terminal: ITerminal) { + super(); this._accessibilityTreeRoot = document.createElement('div'); this._accessibilityTreeRoot.classList.add('xterm-accessibility'); @@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable { this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); - this._disposables.push(this._renderRowsDebouncer); - this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows))); - this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end))); - this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); + this.register(this._renderRowsDebouncer); + this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows))); + this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end))); + this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run - this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); - this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n'))); - this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); - this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar))); - this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); + this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); + this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n'))); + this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); + this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar))); + this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); // TODO: Maybe renderer should fire an event on terminal when the characters change and that // should be listened to instead? That would mean that the order of events are always // guarenteed - this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions())); - this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions())); + this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions())); + this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions())); // This shouldn't be needed on modern browsers but is present in case the // media query that drives the dprchange event isn't supported - this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions())); + this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); } public dispose(): void { - this._disposables.forEach(d => d.dispose()); - this._disposables.length = 0; + super.dispose(); this._terminal.element.removeChild(this._accessibilityTreeRoot); this._rowElements.length = 0; } diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 82aa0224..316809e2 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; import { Buffer } from './Buffer'; -import { CircularList } from './utils/CircularList'; +import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; const INIT_COLS = 80; diff --git a/src/Buffer.ts b/src/Buffer.ts index 5ac2a4f2..7dc2cc7b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { CircularList } from './utils/CircularList'; +import { CircularList } from './common/CircularList'; import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { EventEmitter } from './EventEmitter'; -import { IDisposable, IMarker } from 'xterm'; +import { IMarker } from 'xterm'; export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -320,14 +320,14 @@ export class Buffer implements IBuffer { public addMarker(y: number): Marker { const marker = new Marker(y); this.markers.push(marker); - marker.disposables.push(this.lines.addDisposableListener('trim', amount => { + marker.register(this.lines.addDisposableListener('trim', amount => { marker.line -= amount; // The marker should be disposed when the line is trimmed from the buffer if (marker.line < 0) { marker.dispose(); } })); - marker.on('dispose', () => this._removeMarker(marker)); + marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker))); return marker; } @@ -342,7 +342,6 @@ export class Marker extends EventEmitter implements IMarker { private _id: number = Marker._nextId++; public isDisposed: boolean = false; - public disposables: IDisposable[] = []; public get id(): number { return this._id; } @@ -357,8 +356,8 @@ export class Marker extends EventEmitter implements IMarker { return; } this.isDisposed = true; - this.disposables.forEach(d => d.dispose()); - this.disposables.length = 0; + // Emit before super.dispose such that dispose listeners get a change to react this.emit('dispose'); + super.dispose(); } } diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 2849dbc1..8995b203 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -4,6 +4,7 @@ */ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { Disposable } from './common/Lifecycle'; /** * Returns an array filled with numbers between the low and high parameters (right exclusive). @@ -207,7 +208,7 @@ class DcsDummy implements IDcsHandler { * NOTE: The parameter element notation is currently not supported. * TODO: implement error recovery hook via error handler return values */ -export class EscapeSequenceParser implements IEscapeSequenceParser { +export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser { public initialState: number; public currentState: number; @@ -236,6 +237,8 @@ export class EscapeSequenceParser implements IEscapeSequenceParser { protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { + super(); + this.initialState = ParserState.GROUND; this.currentState = this.initialState; this._osc = ''; @@ -260,6 +263,24 @@ export class EscapeSequenceParser implements IEscapeSequenceParser { this._errorHandler = this._errorHandlerFb; } + public dispose(): void { + this._printHandlerFb = null; + this._executeHandlerFb = null; + this._csiHandlerFb = null; + this._escHandlerFb = null; + this._oscHandlerFb = null; + this._dcsHandlerFb = null; + this._errorHandlerFb = null; + this._printHandler = null; + this._executeHandlers = null; + this._csiHandlers = null; + this._escHandlers = null; + this._oscHandlers = null; + this._dcsHandlers = null; + this._activeDcsHandler = null; + this._errorHandler = null; + } + setPrintHandler(callback: (data: string, start: number, end: number) => void): void { this._printHandler = callback; } diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 9ce31bb5..0de5999a 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -5,11 +5,13 @@ import { XtermListener } from './Types'; import { IEventEmitter, IDisposable } from 'xterm'; +import { Disposable } from './common/Lifecycle'; -export class EventEmitter implements IEventEmitter, IDisposable { +export class EventEmitter extends Disposable implements IEventEmitter, IDisposable { private _events: {[type: string]: XtermListener[]}; constructor() { + super(); // Restore the previous events if available, this will happen if the // constructor is called multiple times on the same object (terminal reset). this._events = this._events || {}; @@ -26,6 +28,7 @@ export class EventEmitter implements IEventEmitter, IDisposable { * @param handler The handler for the listener. */ public addDisposableListener(type: string, handler: XtermListener): IDisposable { + // TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener this.on(type, handler); return { dispose: () => { @@ -76,6 +79,7 @@ export class EventEmitter implements IEventEmitter, IDisposable { } public dispose(): void { + super.dispose(); this._events = {}; } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7c9f730f..bd9ea0c9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; +import { Disposable } from './common/Lifecycle'; /** * Map collect to glevel. Used in `selectCharset`. @@ -111,13 +112,17 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler implements IInputHandler { +export class InputHandler extends Disposable implements IInputHandler { private _surrogateHigh: string; constructor( private _terminal: any, // TODO: reestablish IInputHandlingTerminal here private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { + super(); + + this.register(this._parser); + this._surrogateHigh = ''; /** @@ -285,6 +290,11 @@ export class InputHandler implements IInputHandler { this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); } + public dispose(): void { + super.dispose(); + this._terminal = null; + } + public parse(data: string): void { let buffer = this._terminal.buffer; const cursorStartX = buffer.x; diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 4c0415de..7f4d5603 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -4,11 +4,11 @@ */ import { assert } from 'chai'; -import { IMouseZoneManager, IMouseZone } from './input/Types'; +import { IMouseZoneManager, IMouseZone } from './ui/Types'; import { ILinkMatcher, LineData, ITerminal } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; -import { CircularList } from './utils/CircularList'; +import { CircularList } from './common/CircularList'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { @@ -21,6 +21,8 @@ class TestLinkifier extends Linkifier { } class TestMouseZoneManager implements IMouseZoneManager { + dispose(): void { + } public clears: number = 0; public zones: IMouseZone[] = []; add(zone: IMouseZone): void { diff --git a/src/Linkifier.ts b/src/Linkifier.ts index e548551a..88ff1b78 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { IMouseZoneManager } from './input/Types'; +import { IMouseZoneManager } from './ui/Types'; import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types'; -import { MouseZone } from './input/MouseZoneManager'; +import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; /** diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 8e89ea3f..d7afad70 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { CharMeasure } from './utils/CharMeasure'; +import { CharMeasure } from './ui/CharMeasure'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1b056e83..6521a209 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -6,7 +6,7 @@ import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types'; import { MouseHelper } from './utils/MouseHelper'; import * as Browser from './shared/utils/Browser'; -import { CharMeasure } from './utils/CharMeasure'; +import { CharMeasure } from './ui/CharMeasure'; import { EventEmitter } from './EventEmitter'; import { SelectionModel } from './SelectionModel'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; @@ -116,6 +116,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._activeSelectionMode = SelectionMode.NORMAL; } + public dispose(): void { + super.dispose(); + this._removeMouseDownListeners(); + } + private get _buffer(): IBuffer { return this._terminal.buffers.active; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 9f581461..971c0fc9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -22,7 +22,7 @@ */ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types'; -import { IMouseZoneManager } from './input/Types'; +import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer'; @@ -36,17 +36,17 @@ import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; -import { CharMeasure } from './utils/CharMeasure'; +import { CharMeasure } from './ui/CharMeasure'; import * as Browser from './shared/utils/Browser'; -import * as Dom from './utils/Dom'; +import { addDisposableDomListener } from './ui/Lifecycle'; import * as Strings from './Strings'; import { MouseHelper } from './utils/MouseHelper'; import { clone } from './utils/Clone'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; -import { MouseZoneManager } from './input/MouseZoneManager'; +import { MouseZoneManager } from './ui/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; -import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; +import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; import { ITheme, IMarker, IDisposable } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; @@ -113,8 +113,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public element: HTMLElement; public screenElement: HTMLElement; - private _disposables: IDisposable[]; - /** * The HTMLElement that the terminal is created in, set by Terminal.open. */ @@ -235,8 +233,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public dispose(): void { super.dispose(); - this._disposables.forEach(d => d.dispose()); - this._disposables.length = 0; + this._customKeyEventHandler = null; removeTerminalFromCache(this); this.handler = () => {}; this.write = () => {}; @@ -253,8 +250,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } private _setup(): void { - this._disposables = []; - Object.keys(DEFAULT_OPTIONS).forEach((key) => { if (this.options[key] == null) { this.options[key] = DEFAULT_OPTIONS[key]; @@ -307,6 +302,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._userScrolling = false; this._inputHandler = new InputHandler(this); + this.register(this._inputHandler); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -527,30 +523,30 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._bindKeys(); // Bind clipboard functionality - on(this.element, 'copy', (event: ClipboardEvent) => { + this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => { // If mouse events are active it means the selection manager is disabled and // copy should be handled by the host program. if (!this.hasSelection()) { return; } copyHandler(event, this, this.selectionManager); - }); + })); const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this); - on(this.textarea, 'paste', pasteHandlerWrapper); - on(this.element, 'paste', pasteHandlerWrapper); + this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); + this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); // Handle right click context menus if (Browser.isFirefox) { // Firefox doesn't appear to fire the contextmenu event on right click - on(this.element, 'mousedown', (event: MouseEvent) => { + this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); } - }); + })); } else { - on(this.element, 'contextmenu', (event: MouseEvent) => { + this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); - }); + })); } // Move the textarea under the cursor when middle clicking on Linux to ensure @@ -559,11 +555,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (Browser.isLinux) { // Use auxclick event over mousedown the latter doesn't seem to work. Note // that the regular click event doesn't fire for the middle mouse button. - on(this.element, 'auxclick', (event: MouseEvent) => { + this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { moveTextAreaUnderMouseCursor(event, this.textarea); } - }); + })); } } @@ -572,33 +568,33 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ private _bindKeys(): void { const self = this; - on(this.element, 'keydown', function (ev: KeyboardEvent): void { + this.register(addDisposableDomListener(this.element, 'keydown', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { return; } self._keyDown(ev); - }, true); + }, true)); - on(this.element, 'keypress', function (ev: KeyboardEvent): void { + this.register(addDisposableDomListener(this.element, 'keypress', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { return; } self._keyPress(ev); - }, true); + }, true)); - on(this.element, 'keyup', (ev: KeyboardEvent) => { + this.register(addDisposableDomListener(this.element, 'keyup', (ev: KeyboardEvent) => { if (!wasMondifierKeyOnlyEvent(ev)) { this.focus(); } - }, true); + }, true)); - on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true); - on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true); - on(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()); - on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)); - on(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()); - this.on('refresh', () => this._compositionHelper.updateCompositionElements()); - this.on('refresh', (data) => this._queueLinkification(data.start, data.end)); + this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true)); + this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true)); + this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart())); + this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e))); + this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend())); + this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements())); + this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end))); } /** @@ -619,6 +615,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio)); + this.register(this._screenDprMonitor); // Create main element container this.element = this._document.createElement('div'); @@ -648,7 +645,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II fragment.appendChild(this.screenElement); this._mouseZoneManager = new MouseZoneManager(this); - this.on('scroll', () => this._mouseZoneManager.clearAll()); + this.register(this._mouseZoneManager); + this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this._mouseZoneManager); this.textarea = document.createElement('textarea'); @@ -660,8 +658,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('spellcheck', 'false'); this.textarea.tabIndex = 0; - this.textarea.addEventListener('focus', () => this._onTextAreaFocus()); - this.textarea.addEventListener('blur', () => this._onTextAreaBlur()); + this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus())); + this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this._helperContainer.appendChild(this.textarea); this._compositionView = document.createElement('div'); @@ -679,37 +677,39 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } + this.register(this.renderer); this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); + this.register(this.viewport); - this.on('cursormove', () => this.renderer.onCursorMove()); - this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); - this.on('blur', () => this.renderer.onBlur()); - this.on('focus', () => this.renderer.onFocus()); - this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)); + this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove())); + this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows))); + this.register(this.addDisposableListener('blur', () => this.renderer.onBlur())); + this.register(this.addDisposableListener('focus', () => this.renderer.onFocus())); + this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio))); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. - this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); - this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged()); - this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); + this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); + this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged())); + this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea())); this.selectionManager = new SelectionManager(this, this.charMeasure); - this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); - this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end)); - this.selectionManager.on('newselection', text => { + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); + this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end))); + this.register(this.selectionManager.addDisposableListener('newselection', text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired // only on Linux to enable middle click to paste selection. this.textarea.value = text; this.textarea.focus(); this.textarea.select(); - }); - this.on('scroll', () => { + })); + this.register(this.addDisposableListener('scroll', () => { this.viewport.syncScrollArea(); this.selectionManager.refresh(); - }); - this._viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + })); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); this.mouseHelper = new MouseHelper(this.renderer); @@ -979,7 +979,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return button; } - on(el, 'mousedown', (ev: MouseEvent) => { + this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { // Prevent the focus on the textarea from getting lost // and make sure we get focused on mousedown @@ -1004,30 +1004,37 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return this.cancel(ev); } + // TODO: these event listeners should be managed by the disposable, the Terminal reference may + // be kept aroud if Terminal.dispose is fired when the mouse is down + // bind events - if (this.normalMouse) on(this._document, 'mousemove', sendMove); + if (this.normalMouse) { + this._document.addEventListener('mousemove', sendMove); + } // x10 compatibility mode can't send button releases if (!this.x10Mouse) { const handler = (ev: MouseEvent) => { sendButton(ev); // TODO: Seems dangerous calling this on document? - if (this.normalMouse) off(this._document, 'mousemove', sendMove); - off(this._document, 'mouseup', handler); + if (this.normalMouse) { + this._document.removeEventListener('mousemove', sendMove); + } + this._document.removeEventListener('mouseup', handler); return this.cancel(ev); }; // TODO: Seems dangerous calling this on document? - on(this._document, 'mouseup', handler); + this._document.addEventListener('mouseup', handler); } return this.cancel(ev); - }); + })); // if (this.normalMouse) { // on(this.document, 'mousemove', sendMove); // } - on(el, 'wheel', (ev: WheelEvent) => { + this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { if (!this.mouseEvents) { // Convert wheel events into up/down events when the buffer does not have scrollback, this // enables scrolling in apps hosted in the alt buffer such as vim or tmux. @@ -1052,27 +1059,27 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.x10Mouse || this._vt300Mouse || this._decLocator) return; sendButton(ev); ev.preventDefault(); - }); + })); // allow wheel scrolling in // the shell for example - on(el, 'wheel', (ev: WheelEvent) => { + this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { if (this.mouseEvents) return; this.viewport.onWheel(ev); return this.cancel(ev); - }); + })); - on(el, 'touchstart', (ev: TouchEvent) => { + this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { if (this.mouseEvents) return; this.viewport.onTouchStart(ev); return this.cancel(ev); - }); + })); - on(el, 'touchmove', (ev: TouchEvent) => { + this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { if (this.mouseEvents) return; this.viewport.onTouchMove(ev); return this.cancel(ev); - }); + })); } /** @@ -1916,21 +1923,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Helpers */ -function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean, passive?: boolean): void { - if (!Array.isArray(el)) { - el = [el]; - } - el.forEach((element: HTMLElement) => { - element.addEventListener(type, handler, { capture: capture || false, passive: passive || false }); - }); -} -// TODO: Remove once everything is typed -const on = globalOn; - -function off(el: any, type: string, handler: (event: Event) => any, capture: boolean = false): void { - el.removeEventListener(type, handler, capture); -} - function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { return ev.keyCode === 16 || // Shift ev.keyCode === 17 || // Ctrl diff --git a/src/Types.ts b/src/Types.ts index 1de223f7..f3a84550 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm'; +import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm'; import { IColorSet, IRenderer } from './renderer/Types'; -import { IMouseZoneManager } from './input/Types'; +import { IMouseZoneManager } from './ui/Types'; import { ICharset } from './core/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -86,7 +86,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { tabSet(): void; } -export interface IViewport { +export interface IViewport extends IDisposable { scrollBarWidth: number; syncScrollArea(): void; getLinesScrolled(ev: WheelEvent): number; @@ -468,7 +468,7 @@ export interface IDcsHandler { /** * EscapeSequenceParser interface. */ -export interface IEscapeSequenceParser { +export interface IEscapeSequenceParser extends IDisposable { /** * Reset the parser to its initial state (handlers are kept). */ diff --git a/src/Viewport.ts b/src/Viewport.ts index f690c348..a8966d14 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -5,7 +5,9 @@ import { IColorSet } from './renderer/Types'; import { ITerminal, IViewport } from './Types'; -import { CharMeasure } from './utils/CharMeasure'; +import { CharMeasure } from './ui/CharMeasure'; +import { Disposable } from './common/Lifecycle'; +import { addDisposableDomListener } from './ui/Lifecycle'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -13,7 +15,7 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15; * Represents the viewport of a terminal, the visible area within the larger buffer of output. * Logic for the virtual scroll bar is included in this object. */ -export class Viewport implements IViewport { +export class Viewport extends Disposable implements IViewport { public scrollBarWidth: number = 0; private _currentRowHeight: number = 0; private _lastRecordedBufferLength: number = 0; @@ -39,11 +41,13 @@ export class Viewport implements IViewport { private _scrollArea: HTMLElement, private _charMeasure: CharMeasure ) { + super(); + // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar. // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); // Perform this async to ensure the CharMeasure is ready. setTimeout(() => this.syncScrollArea(), 0); diff --git a/src/addons/attach/Interfaces.ts b/src/addons/attach/Interfaces.ts index ab809af9..ab5846f5 100644 --- a/src/addons/attach/Interfaces.ts +++ b/src/addons/attach/Interfaces.ts @@ -5,9 +5,13 @@ * Implements the attach method, that attaches the terminal to a WebSocket stream. */ -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; export interface IAttachAddonTerminal extends Terminal { + _core: { + register(d: T): void; + }; + __socket?: WebSocket; __attachSocketBuffer?: string; diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index e6d92b56..98a0bfaa 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -5,7 +5,7 @@ * Implements the attach method, that attaches the terminal to a WebSocket stream. */ -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; import { IAttachAddonTerminal } from './Interfaces'; /** @@ -87,14 +87,28 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean socket.send(data); }; - socket.addEventListener('message', addonTerminal.__getMessage); + addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage)); if (bidirectional) { - addonTerminal.on('data', addonTerminal.__sendData); + addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData)); } - socket.addEventListener('close', () => detach(addonTerminal, socket)); - socket.addEventListener('error', () => detach(addonTerminal, socket)); + addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket))); + addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket))); +} + +function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable { + socket.addEventListener(type, handler); + return { + dispose: () => { + if (!handler) { + // Already disposed + return; + } + socket.removeEventListener(type, handler); + handler = null; + } + }; } /** diff --git a/src/utils/CircularList.test.ts b/src/common/CircularList.test.ts similarity index 100% rename from src/utils/CircularList.test.ts rename to src/common/CircularList.test.ts diff --git a/src/utils/CircularList.ts b/src/common/CircularList.ts similarity index 100% rename from src/utils/CircularList.ts rename to src/common/CircularList.ts diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts new file mode 100644 index 00000000..b9cf1418 --- /dev/null +++ b/src/common/Lifecycle.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'xterm'; + +/** + * A base class that can be extended to provide convenience methods for managing the lifecycle of an + * object and its components. + */ +export abstract class Disposable implements IDisposable { + protected _disposables: IDisposable[] = []; + + constructor() { + } + + /** + * Disposes the object, triggering the `dispose` method on all registered IDisposables. + */ + public dispose(): void { + this._disposables.forEach(d => d.dispose()); + this._disposables.length = 0; + } + + /** + * Registers a disposable object. + * @param d The disposable to register. + */ + public register(d: T): void { + this._disposables.push(d); + } +} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 76e7fba0..a56a270a 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -45,7 +45,7 @@ export class Terminal implements ITerminalApi { this._core.emit(type, data); } public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable { - return this.addDisposableListener(type, handler); + return this._core.addDisposableListener(type, handler); } public resize(columns: number, rows: number): void { this._core.resize(columns, rows); diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index c41dece5..aecd5c79 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -11,8 +11,8 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types'; import { ITerminal } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; import { EventEmitter } from '../EventEmitter'; -import { RenderDebouncer } from '../utils/RenderDebouncer'; -import { ScreenDprMonitor } from '../utils/ScreenDprMonitor'; +import { RenderDebouncer } from '../ui/RenderDebouncer'; +import { ScreenDprMonitor } from '../ui/ScreenDprMonitor'; import { ITheme } from 'xterm'; export class Renderer extends EventEmitter implements IRenderer { @@ -62,12 +62,14 @@ export class Renderer extends EventEmitter implements IRenderer { this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio)); + this.register(this._screenDprMonitor); // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0}); observer.observe(this._terminal.element); + this.register({ dispose: () => observer.disconnect() }); } } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index bafc9ad4..e7054c13 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -4,7 +4,7 @@ */ import { ITerminal } from '../Types'; -import { IEventEmitter, ITheme } from 'xterm'; +import { IEventEmitter, ITheme, IDisposable } from 'xterm'; import { IColorSet } from '../shared/Types'; /** @@ -24,10 +24,11 @@ export const enum FLAGS { * Note that IRenderer implementations should emit the refresh event after * rendering rows to the screen. */ -export interface IRenderer extends IEventEmitter { +export interface IRenderer extends IEventEmitter, IDisposable { dimensions: IRenderDimensions; colorManager: IColorManager; + dispose(): void; setTheme(theme: ITheme): IColorSet; onWindowResize(devicePixelRatio: number): void; onResize(cols: number, rows: number): void; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 8c335dca..989e147d 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -8,7 +8,7 @@ import { ITerminal } from '../../Types'; import { ITheme } from 'xterm'; import { EventEmitter } from '../../EventEmitter'; import { ColorManager } from '../ColorManager'; -import { RenderDebouncer } from '../../utils/RenderDebouncer'; +import { RenderDebouncer } from '../../ui/RenderDebouncer'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; diff --git a/src/utils/CharMeasure.test.ts b/src/ui/CharMeasure.test.ts similarity index 100% rename from src/utils/CharMeasure.test.ts rename to src/ui/CharMeasure.test.ts diff --git a/src/utils/CharMeasure.ts b/src/ui/CharMeasure.ts similarity index 100% rename from src/utils/CharMeasure.ts rename to src/ui/CharMeasure.ts diff --git a/src/utils/Dom.ts b/src/ui/Lifecycle.ts similarity index 94% rename from src/utils/Dom.ts rename to src/ui/Lifecycle.ts index 889c88c2..d8367113 100644 --- a/src/utils/Dom.ts +++ b/src/ui/Lifecycle.ts @@ -10,7 +10,7 @@ import { IDisposable } from 'xterm'; * @param type The event type. * @param handler The handler for the listener. */ -export function addDisposableListener( +export function addDisposableDomListener( node: Element | Window | Document, type: string, handler: (e: any) => void, diff --git a/src/input/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts similarity index 94% rename from src/input/MouseZoneManager.ts rename to src/ui/MouseZoneManager.ts index 65fe74de..491e2a05 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -5,6 +5,8 @@ import { ITerminal } from '../Types'; import { IMouseZoneManager, IMouseZone } from './Types'; +import { Disposable } from '../common/Lifecycle'; +import { addDisposableDomListener } from './Lifecycle'; const HOVER_DURATION = 500; @@ -16,7 +18,7 @@ const HOVER_DURATION = 500; * needed to support was single-line links which never overlap. Improvements can * be made in the future. */ -export class MouseZoneManager implements IMouseZoneManager { +export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _zones: IMouseZone[] = []; private _areZonesActive: boolean = false; @@ -30,13 +32,20 @@ export class MouseZoneManager implements IMouseZoneManager { constructor( private _terminal: ITerminal ) { - this._terminal.element.addEventListener('mousedown', e => this._onMouseDown(e)); + super(); + + this.register(addDisposableDomListener(this._terminal.element, 'mousedown', e => this._onMouseDown(e))); // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); this._clickListener = e => this._onClick(e); } + public dispose(): void { + super.dispose(); + this._deactivate(); + } + public add(zone: IMouseZone): void { this._zones.push(zone); if (this._zones.length === 1) { diff --git a/src/utils/RenderDebouncer.ts b/src/ui/RenderDebouncer.ts similarity index 100% rename from src/utils/RenderDebouncer.ts rename to src/ui/RenderDebouncer.ts diff --git a/src/utils/ScreenDprMonitor.ts b/src/ui/ScreenDprMonitor.ts similarity index 91% rename from src/utils/ScreenDprMonitor.ts rename to src/ui/ScreenDprMonitor.ts index 15f3ac00..9247a032 100644 --- a/src/utils/ScreenDprMonitor.ts +++ b/src/ui/ScreenDprMonitor.ts @@ -3,6 +3,8 @@ * @license MIT */ +import { Disposable } from '../common/Lifecycle'; + export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void; /** @@ -15,7 +17,7 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat * The listener should fire on both window zoom changes and switching to a * monitor with a different DPI. */ -export class ScreenDprMonitor { +export class ScreenDprMonitor extends Disposable { private _currentDevicePixelRatio: number; private _outerListener: MediaQueryListListener; private _listener: ScreenDprListener; @@ -33,6 +35,11 @@ export class ScreenDprMonitor { this._updateDpr(); } + public dispose(): void { + super.dispose(); + this.clearListener(); + } + private _updateDpr(): void { // Clear listeners for old DPR if (this._resolutionMediaMatchList) { diff --git a/src/input/Types.ts b/src/ui/Types.ts similarity index 83% rename from src/input/Types.ts rename to src/ui/Types.ts index 2bd805bf..89dfa1a4 100644 --- a/src/input/Types.ts +++ b/src/ui/Types.ts @@ -3,7 +3,9 @@ * @license MIT */ -export interface IMouseZoneManager { +import { IDisposable } from 'xterm'; + +export interface IMouseZoneManager extends IDisposable { add(zone: IMouseZone): void; clearAll(start?: number, end?: number): void; } diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 763eb3e6..59b55cf3 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -316,6 +316,9 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + dispose(): void { + throw new Error('Method not implemented.'); + } colorManager: IColorManager; on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); @@ -344,6 +347,9 @@ export class MockRenderer implements IRenderer { } export class MockViewport implements IViewport { + dispose(): void { + throw new Error('Method not implemented.'); + } scrollBarWidth: number = 0; onThemeChanged(colors: IColorSet): void { throw new Error('Method not implemented.');