diff --git a/src/vscode-mock.ts b/src/vscode-mock.ts index c0ca4b2..3b67f91 100644 --- a/src/vscode-mock.ts +++ b/src/vscode-mock.ts @@ -36,6 +36,7 @@ import { EndOfLine, EnvironmentVariableMutatorType, EvaluatableExpression, + EventEmitter, ExtensionKind, ExtensionMode, FileChangeType, @@ -94,7 +95,6 @@ type NotImplemented = | 'CancellationError' | 'CancellationTokenSource' | 'CodeActionTriggerKind' - | 'EventEmitter' | 'Hover' | 'InlineValueText' | 'InlineValueVariableLookup' @@ -245,6 +245,7 @@ export function createVSCodeMock(jest: TestFramework): VSCodeMock { EndOfLine, EnvironmentVariableMutatorType, EvaluatableExpression, + EventEmitter, ExtensionKind, ExtensionMode, FileChangeType, diff --git a/src/vscode/EventEmitter.test.ts b/src/vscode/EventEmitter.test.ts new file mode 100644 index 0000000..ab9052e --- /dev/null +++ b/src/vscode/EventEmitter.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, jest, test } from '@jest/globals'; + +import { EventEmitter } from './EventEmitter'; + +describe('EventEmitter', () => { + test('fire', () => { + const emitter = new EventEmitter(); + const listener = jest.fn(); + const listener2 = jest.fn(); + emitter.event(listener); + emitter.event(listener2); + emitter.fire('foo'); + expect(listener).toHaveBeenCalledWith('foo'); + expect(listener2).toHaveBeenCalledWith('foo'); + }); + + test('dispose', () => { + const emitter = new EventEmitter(); + const listener = jest.fn(); + const disposable = emitter.event(listener); + emitter.fire('foo'); + disposable.dispose(); + emitter.fire('bar'); + expect(listener).toHaveBeenCalledWith('foo'); + }); +}); diff --git a/src/vscode/EventEmitter.ts b/src/vscode/EventEmitter.ts new file mode 100644 index 0000000..92dfda7 --- /dev/null +++ b/src/vscode/EventEmitter.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable-next-line node/no-missing-import +import type * as vscode from 'vscode'; + +type Disposable = vscode.Disposable; + +type Listener = (e: T) => any; + +export class EventEmitter implements vscode.EventEmitter { + readonly #listeners = new Set>(); + + /** + * The event listeners can subscribe to. + */ + event(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable { + const fn = thisArgs ? listener.bind(thisArgs) : listener; + this.#listeners.add(fn); + const disposable = { + dispose: () => { + this.#listeners.delete(fn); + }, + }; + + if (disposables) { + disposables.push(disposable); + } + + return disposable; + } + + /** + * Notify all subscribers of the {@link EventEmitter.event event}. Failure + * of one or more listener will not fail this function call. + * + * @param data The event object. + */ + fire(data: T): void { + for (const listener of this.#listeners) { + try { + listener(data); + } catch { + // ignore + } + } + } + + /** + * Dispose this object and free resources. + */ + dispose(): void { + this.#listeners.clear(); + } + + constructor() {} +} diff --git a/src/vscode/index.ts b/src/vscode/index.ts index a0f00c5..67a5064 100644 --- a/src/vscode/index.ts +++ b/src/vscode/index.ts @@ -21,3 +21,4 @@ export * from './uri'; export { createWindow } from './window'; export type { Window } from './window'; export { MockWorkspace, Workspace, createWorkspace } from './workspace'; +export { EventEmitter } from './EventEmitter';