/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import CallbackList from 'vs/base/common/callbackList'; import { EventEmitter } from 'vs/base/common/eventEmitter'; import { TPromise } from 'vs/base/common/winjs.base'; import { once as onceFn } from 'vs/base/common/functional'; /** * To an event a function with one or zero parameters * can be subscribed. The event is the subscriber function itself. */ interface Event { (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } namespace Event { const _disposable = { dispose() { } }; export const None: Event = function () { return _disposable; }; } export default Event; export interface EmitterOptions { onFirstListenerAdd?: Function; onFirstListenerDidAdd?: Function; onListenerDidAdd?: Function; onLastListenerRemove?: Function; } /** * The Emitter can be used to expose an Event to the public * to fire it from the insides. * Sample: class Document { private _onDidChange = new Emitter<(value:string)=>any>(); public onDidChange = this._onDidChange.event; // getter-style // get onDidChange(): Event<(value:string)=>any> { // return this._onDidChange.event; // } private _doIt() { //... this._onDidChange.fire(value); } } */ export class Emitter { private static _noop = function () { }; private _event: Event; private _callbacks: CallbackList; private _disposed: boolean; constructor(private _options?: EmitterOptions) { } /** * For the public to allow to subscribe * to events from this Emitter */ get event(): Event { if (!this._event) { this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]) => { if (!this._callbacks) { this._callbacks = new CallbackList(); } const firstListener = this._callbacks.isEmpty(); if (firstListener && this._options && this._options.onFirstListenerAdd) { this._options.onFirstListenerAdd(this); } this._callbacks.add(listener, thisArgs); if (firstListener && this._options && this._options.onFirstListenerDidAdd) { this._options.onFirstListenerDidAdd(this); } if (this._options && this._options.onListenerDidAdd) { this._options.onListenerDidAdd(this, listener, thisArgs); } let result: IDisposable; result = { dispose: () => { result.dispose = Emitter._noop; if (!this._disposed) { this._callbacks.remove(listener, thisArgs); if (this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) { this._options.onLastListenerRemove(this); } } } }; if (Array.isArray(disposables)) { disposables.push(result); } return result; }; } return this._event; } /** * To be kept private to fire an event to * subscribers */ fire(event?: T): any { if (this._callbacks) { this._callbacks.invoke.call(this._callbacks, event); } } dispose() { if (this._callbacks) { this._callbacks.dispose(); this._callbacks = undefined; this._disposed = true; } } } export class EventMultiplexer implements IDisposable { private emitter: Emitter; private hasListeners = false; private events: { event: Event; listener: IDisposable; }[] = []; constructor() { this.emitter = new Emitter({ onFirstListenerAdd: () => this.onFirstListenerAdd(), onLastListenerRemove: () => this.onLastListenerRemove() }); } get event(): Event { return this.emitter.event; } add(event: Event): IDisposable { const e = { event: event, listener: null }; this.events.push(e); if (this.hasListeners) { this.hook(e); } const dispose = () => { if (this.hasListeners) { this.unhook(e); } const idx = this.events.indexOf(e); this.events.splice(idx, 1); }; return toDisposable(onceFn(dispose)); } private onFirstListenerAdd(): void { this.hasListeners = true; this.events.forEach(e => this.hook(e)); } private onLastListenerRemove(): void { this.hasListeners = false; this.events.forEach(e => this.unhook(e)); } private hook(e: { event: Event; listener: IDisposable; }): void { e.listener = e.event(r => this.emitter.fire(r)); } private unhook(e: { event: Event; listener: IDisposable; }): void { e.listener.dispose(); e.listener = null; } dispose(): void { this.emitter.dispose(); } } /** * Creates an Event which is backed-up by the event emitter. This allows * to use the existing eventing pattern and is likely using less memory. * Sample: * * class Document { * * private _eventbus = new EventEmitter(); * * public onDidChange = fromEventEmitter(this._eventbus, 'changed'); * * // getter-style * // get onDidChange(): Event<(value:string)=>any> { * // cache fromEventEmitter result and return * // } * * private _doIt() { * // ... * this._eventbus.emit('changed', value) * } * } */ export function fromEventEmitter(emitter: EventEmitter, eventType: string): Event { return function (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable { const result = emitter.addListener(eventType, function () { listener.apply(thisArgs, arguments); }); if (Array.isArray(disposables)) { disposables.push(result); } return result; }; } export function fromCallback(fn: (handler: (e: T) => void) => IDisposable): Event { let listener: IDisposable; const emitter = new Emitter({ onFirstListenerAdd: () => listener = fn(e => emitter.fire(e)), onLastListenerRemove: () => listener.dispose() }); return emitter.event; } export function fromPromise(promise: TPromise): Event { const emitter = new Emitter(); let shouldEmit = false; promise .then(null, () => null) .then(() => { if (!shouldEmit) { setTimeout(() => emitter.fire(), 0); } else { emitter.fire(); } }); shouldEmit = true; return emitter.event; } export function toPromise(event: Event): TPromise { return new TPromise(complete => { const sub = event(e => { sub.dispose(); complete(e); }); }); } export function delayed(promise: TPromise>): Event { let toCancel: TPromise = null; let listener: IDisposable = null; const emitter = new Emitter({ onFirstListenerAdd() { toCancel = promise.then( event => listener = event(e => emitter.fire(e)), () => null ); }, onLastListenerRemove() { if (toCancel) { toCancel.cancel(); toCancel = null; } if (listener) { listener.dispose(); listener = null; } } }); return emitter.event; } export function once(event: Event): Event { return (listener, thisArgs = null, disposables?) => { const result = event(e => { result.dispose(); return listener.call(thisArgs, e); }, null, disposables); return result; }; } export function any(...events: Event[]): Event { return (listener, thisArgs = null, disposables?) => combinedDisposable(events.map(event => event(e => listener.call(thisArgs, e), null, disposables))); } export function debounceEvent(event: Event, merger: (last: T, event: T) => T, delay?: number, leading?: boolean): Event; export function debounceEvent(event: Event, merger: (last: O, event: I) => O, delay?: number, leading?: boolean): Event; export function debounceEvent(event: Event, merger: (last: O, event: I) => O, delay: number = 100, leading = false): Event { let subscription: IDisposable; let output: O; let handle: number; const emitter = new Emitter({ onFirstListenerAdd() { subscription = event(cur => { output = merger(output, cur); if (!handle && leading) { emitter.fire(output); } clearTimeout(handle); handle = setTimeout(() => { let _output = output; output = undefined; emitter.fire(_output); handle = null; }, delay); }); }, onLastListenerRemove() { subscription.dispose(); } }); return emitter.event; } /** * The EventDelayer is useful in situations in which you want * to delay firing your events during some code. * You can wrap that code and be sure that the event will not * be fired during that wrap. * * ``` * const emitter: Emitter; * const delayer = new EventDelayer(); * const delayedEvent = delayer.wrapEvent(emitter.event); * * delayedEvent(console.log); * * delayer.bufferEvents(() => { * emitter.fire(); // event will not be fired yet * }); * * // event will only be fired at this point * ``` */ export class EventBufferer { private buffers: Function[][] = []; wrapEvent(event: Event): Event { return (listener, thisArgs?, disposables?) => { return event(i => { const buffer = this.buffers[this.buffers.length - 1]; if (buffer) { buffer.push(() => listener.call(thisArgs, i)); } else { listener.call(thisArgs, i); } }, void 0, disposables); }; } bufferEvents(fn: () => void): void { const buffer: Function[] = []; this.buffers.push(buffer); fn(); this.buffers.pop(); buffer.forEach(flush => flush()); } } export interface IChainableEvent { event: Event; map(fn: (i: T) => O): IChainableEvent; filter(fn: (e: T) => boolean): IChainableEvent; on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } export function mapEvent(event: Event, map: (i: I) => O): Event { return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables); } export function filterEvent(event: Event, filter: (e: T) => boolean): Event { return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables); } class ChainableEvent implements IChainableEvent { get event(): Event { return this._event; } constructor(private _event: Event) { } map(fn: (i: T) => O): IChainableEvent { return new ChainableEvent(mapEvent(this._event, fn)); } filter(fn: (e: T) => boolean): IChainableEvent { return new ChainableEvent(filterEvent(this._event, fn)); } on(listener, thisArgs, disposables: IDisposable[]) { return this._event(listener, thisArgs, disposables); } } export function chain(event: Event): IChainableEvent { return new ChainableEvent(event); } export function stopwatch(event: Event): Event { const start = new Date().getTime(); return mapEvent(once(event), _ => new Date().getTime() - start); } /** * Buffers the provided event until a first listener comes * along, at which point fire all the events at once and * pipe the event from then on. * * ```typescript * const emitter = new Emitter(); * const event = emitter.event; * const bufferedEvent = buffer(event); * * emitter.fire(1); * emitter.fire(2); * emitter.fire(3); * // nothing... * * const listener = bufferedEvent(num => console.log(num)); * // 1, 2, 3 * * emitter.fire(4); * // 4 * ``` */ export function buffer(event: Event, nextTick = false, buffer: T[] = []): Event { buffer = buffer.slice(); let listener = event(e => { if (buffer) { buffer.push(e); } else { emitter.fire(e); } }); const flush = () => { buffer.forEach(e => emitter.fire(e)); buffer = null; }; const emitter = new Emitter({ onFirstListenerAdd() { if (!listener) { listener = event(e => emitter.fire(e)); } }, onFirstListenerDidAdd() { if (buffer) { if (nextTick) { setTimeout(flush); } else { flush(); } } }, onLastListenerRemove() { listener.dispose(); listener = null; } }); return emitter.event; } /** * Similar to `buffer` but it buffers indefinitely and repeats * the buffered events to every new listener. */ export function echo(event: Event, nextTick = false, buffer: T[] = []): Event { buffer = buffer.slice(); event(e => { buffer.push(e); emitter.fire(e); }); const flush = (listener, thisArgs?) => buffer.forEach(e => listener.call(thisArgs, e)); const emitter = new Emitter({ onListenerDidAdd(emitter, listener, thisArgs?) { if (nextTick) { setTimeout(() => flush(listener, thisArgs)); } else { flush(listener, thisArgs); } } }); return emitter.event; }