From 9d74786a623cafb91acf5bbbcdf5631e632b2223 Mon Sep 17 00:00:00 2001 From: Claudia Pellegrino Date: Thu, 23 Feb 2023 21:58:54 +0100 Subject: [PATCH] Simplify, refactor, document --- extension/src/cli-api/impl.ts | 10 ++-- extension/src/events.ts | 22 +++------ extension/src/events/filters.ts | 7 +-- extension/src/events/stream.ts | 28 ++++++----- extension/src/events/throttle.ts | 84 ++++++++++++++++++++++++-------- 5 files changed, 94 insertions(+), 57 deletions(-) diff --git a/extension/src/cli-api/impl.ts b/extension/src/cli-api/impl.ts index 7a7daa9..e3a9225 100644 --- a/extension/src/cli-api/impl.ts +++ b/extension/src/cli-api/impl.ts @@ -6,7 +6,7 @@ import { } from "child_process"; import { promisify } from "util"; -import { Disposable, TextDocument, workspace } from "vscode"; +import { Disposable, Event, TextDocument, workspace } from "vscode"; import { CliOutput, DocumentParsedEvent, Ifm, IfmCli } from "../cli-api"; import { MAX_RUNTIME_MILLIS } from "../constants"; @@ -116,9 +116,7 @@ export class IfmAdapter implements Ifm { } onDidCliChange( - listener: () => void, - thisArgs?: any, - disposables?: Disposable[], + ...[listener, thisArgs, disposables]: Parameters> ) { const subscriptionId: number = this.#nextSubscriptionId++; this.#didCliChangeSubscriptions.set( @@ -173,9 +171,7 @@ export class IfmAdapter implements Ifm { } onDidParseDocument( - listener: (e: DocumentParsedEvent) => void, - thisArgs?: any, - disposables?: Disposable[], + ...[listener, thisArgs, disposables]: Parameters> ) { const subscriptionId: number = this.#nextSubscriptionId++; this.#didParseDocumentSubscriptions.set( diff --git a/extension/src/events.ts b/extension/src/events.ts index fd0c7dc..c88a31a 100644 --- a/extension/src/events.ts +++ b/extension/src/events.ts @@ -2,7 +2,6 @@ import { Disposable, Event, TextDocument, - TextDocumentChangeEvent, workspace, } from "vscode"; import { CHANGE_EVENT_THROTTLE_MILLIS } from "./constants"; @@ -14,13 +13,12 @@ import { excludeIrrelevantTextDocumentsByScheme, ignoreIfAlreadyClosed, } from "./events/filters"; -import { streamEvents } from "./events/stream"; +import { EventStream } from "./events/stream"; import { throttleEvent } from "./events/throttle"; const onDidInitiallyFindTextDocument: Event = ( - listener: (e: TextDocument) => any, - thisArgs?: any, -): Disposable => { + ...[listener, thisArgs]: Parameters> +) => { for (const document of workspace.textDocuments) { listener.call(thisArgs, document); } @@ -28,27 +26,23 @@ const onDidInitiallyFindTextDocument: Event = ( }; export const onDidInitiallyFindRelevantTextDocument: Event = - streamEvents(onDidInitiallyFindTextDocument) + EventStream.of(onDidInitiallyFindTextDocument) .through(excludeIrrelevantTextDocumentsByScheme) .through(excludeIrrelevantTextDocumentsByLanguage); export const onDidChangeRelevantTextDocument: Event = - streamEvents(workspace.onDidChangeTextDocument) + EventStream.of(workspace.onDidChangeTextDocument) .through(excludeIrrelevantChangeEventsByScheme) .through(excludeIrrelevantChangeEventsByLanguage) - .through( - throttleEvent, - CHANGE_EVENT_THROTTLE_MILLIS, - (e: TextDocumentChangeEvent) => e.document, - ) + .map(throttleEvent(CHANGE_EVENT_THROTTLE_MILLIS, (e) => e.document)) .through(ignoreIfAlreadyClosed); export const onDidOpenRelevantTextDocument: Event = - streamEvents(workspace.onDidOpenTextDocument) + EventStream.of(workspace.onDidOpenTextDocument) .through(excludeIrrelevantTextDocumentsByScheme) .through(excludeIrrelevantTextDocumentsByLanguage); export const onDidCloseRelevantTextDocument: Event = - streamEvents(workspace.onDidCloseTextDocument) + EventStream.of(workspace.onDidCloseTextDocument) .through(excludeIrrelevantTextDocumentsByScheme) .through(excludeIrrelevantTextDocumentsByLanguage); diff --git a/extension/src/events/filters.ts b/extension/src/events/filters.ts index 159ad7c..1299b78 100644 --- a/extension/src/events/filters.ts +++ b/extension/src/events/filters.ts @@ -1,5 +1,4 @@ import { - Disposable, DocumentSelector, Event, languages, @@ -74,10 +73,8 @@ export function select( upstreamEvent: Event, ): Event { return ( - listener: (e: T) => any, - listenerThisArgs?: any, - disposables?: Disposable[], - ): Disposable => { + ...[listener, listenerThisArgs, disposables]: Parameters> + ) => { const upstreamListener: (e: T) => any = (e) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return match(e) ? listener.call(listenerThisArgs, e) : null; diff --git a/extension/src/events/stream.ts b/extension/src/events/stream.ts index ea587ef..1c7d194 100644 --- a/extension/src/events/stream.ts +++ b/extension/src/events/stream.ts @@ -6,19 +6,25 @@ interface EventStreamFunction { (...args: [...A, Event]): Event; } -interface EventStream extends Event { +export class EventStream { + event: Event; + + map(fn: (e: Event) => Event): EventStream & Event { + return EventStream.of(fn(this.event)); + } + through( fn: EventStreamFunction, ...args: A - ): EventStream; -} + ): EventStream & Event { + return EventStream.of(fn(...args, this.event)); + } -export function streamEvents(event: Event): EventStream { - const stream: EventStream = function (...args) { - return event(...args); - }; - stream.through = (fn, ...args) => { - return streamEvents(fn(...args, event)); - }; - return stream; + static of(event: Event): EventStream & Event { + const result: EventStream & Event = (...args) => event(...args); + result.map = EventStream.prototype.map; + result.through = EventStream.prototype.through; + result.event = event; + return result; + } } diff --git a/extension/src/events/throttle.ts b/extension/src/events/throttle.ts index a5b0cde..84d3ffc 100644 --- a/extension/src/events/throttle.ts +++ b/extension/src/events/throttle.ts @@ -1,29 +1,73 @@ -import { Disposable, Event } from "vscode"; +import { Event } from "vscode"; import { throttle } from "throttle-debounce"; -export function throttleEvent( +/* + * Each entry in this map corresponds to a single invocation of the + * throttled listener. + * + * A key represents the group of events that will be handed to the + * invocation as an argument. It is defined as the result of invoking + * the `groupBy` function on any member of the group, + * + * A map value is an invocation of `listener`, wrapped in `throttle` + * so that we’re going to invoke `listener` exactly once per entry. + */ +type _ListenerMap = Map void>; + +function _throttleEvent( delayMs: number, groupBy: (event: T) => U, upstreamEvent: Event, -): Event { - return ( - listener: (eventGroup: U) => any, - listenerThisArgs?: any, - disposables?: Disposable[], - ): Disposable => { - const throttledListenersByGroup: Map void> = new Map(); - const upstreamListener: (e: T) => void = (e) => { - const eventGroup: U = groupBy(e); - if (!throttledListenersByGroup.has(eventGroup)) { - throttledListenersByGroup.set( - eventGroup, - throttle(delayMs, listener.bind(listenerThisArgs, eventGroup)), - ); - } - throttledListenersByGroup.get(eventGroup)!(); + ...[listener, listenerThisArgs, disposables]: Parameters> +) { + + // Keys: compound payloads + // Values: throttled invocations of compound events + const listenerMap: _ListenerMap = new Map(); + + function upstreamListener(groupableEvent: T): void { + const key: U = groupBy(groupableEvent); + + if (!listenerMap.has(key)) { + const boundUpstreamListener: () => void = + listener.bind(listenerThisArgs, key); + listenerMap.set(key, throttle(delayMs, boundUpstreamListener)); + } + + const throttledListener: () => void = listenerMap.get(key)!; + throttledListener(); + } + + return upstreamEvent(upstreamListener, null, disposables); +} + +/** + * Fires a single compound event for each group of individual upstream events + * which occur in a given time window. + * + * @param delayMs how long the throttled event should wait before firing. + * + * @param groupBy maps an upstream event to an event group. + * If multiple upstream events share a group and occur within the given + * time window, they will trigger a single compound event. The compound event + * will fire after the time window has elapsed. + * + * @returns a function that transforms a fine-grained VS Code event source + * to a coarser, throttled event source, so that a single compound event fires + * at most every `delayMs` milliseconds for each event group. + * + * @type T Original payload type of the upstream event. + * + * @type U Output payload type, which will be passed as a parameter to the + * compound event when it fires. + * Instances of U may not have internal state, and they cannot be + * mutable. + */ +export function throttleEvent(delayMs: number, groupBy: (event: T) => U) { + return (upstreamEvent: Event) => { + return (...eventArgs: Parameters>) => { + return _throttleEvent(delayMs, groupBy, upstreamEvent, ...eventArgs); }; - return upstreamEvent(upstreamListener, null, disposables); }; } -