Skip to content

Commit

Permalink
Simplify, refactor, document
Browse files Browse the repository at this point in the history
  • Loading branch information
claui committed Mar 2, 2023
1 parent 478164d commit 9d74786
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 57 deletions.
10 changes: 3 additions & 7 deletions extension/src/cli-api/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -116,9 +116,7 @@ export class IfmAdapter implements Ifm {
}

onDidCliChange(
listener: () => void,
thisArgs?: any,
disposables?: Disposable[],
...[listener, thisArgs, disposables]: Parameters<Event<void>>
) {
const subscriptionId: number = this.#nextSubscriptionId++;
this.#didCliChangeSubscriptions.set(
Expand Down Expand Up @@ -173,9 +171,7 @@ export class IfmAdapter implements Ifm {
}

onDidParseDocument(
listener: (e: DocumentParsedEvent) => void,
thisArgs?: any,
disposables?: Disposable[],
...[listener, thisArgs, disposables]: Parameters<Event<DocumentParsedEvent>>
) {
const subscriptionId: number = this.#nextSubscriptionId++;
this.#didParseDocumentSubscriptions.set(
Expand Down
22 changes: 8 additions & 14 deletions extension/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Disposable,
Event,
TextDocument,
TextDocumentChangeEvent,
workspace,
} from "vscode";
import { CHANGE_EVENT_THROTTLE_MILLIS } from "./constants";
Expand All @@ -14,41 +13,36 @@ import {
excludeIrrelevantTextDocumentsByScheme,
ignoreIfAlreadyClosed,
} from "./events/filters";
import { streamEvents } from "./events/stream";
import { EventStream } from "./events/stream";
import { throttleEvent } from "./events/throttle";

const onDidInitiallyFindTextDocument: Event<TextDocument> = (
listener: (e: TextDocument) => any,
thisArgs?: any,
): Disposable => {
...[listener, thisArgs]: Parameters<Event<TextDocument>>
) => {
for (const document of workspace.textDocuments) {
listener.call(thisArgs, document);
}
return Disposable.from();
};

export const onDidInitiallyFindRelevantTextDocument: Event<TextDocument> =
streamEvents(onDidInitiallyFindTextDocument)
EventStream.of(onDidInitiallyFindTextDocument)
.through(excludeIrrelevantTextDocumentsByScheme)
.through(excludeIrrelevantTextDocumentsByLanguage);

export const onDidChangeRelevantTextDocument: Event<TextDocument> =
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<TextDocument> =
streamEvents(workspace.onDidOpenTextDocument)
EventStream.of(workspace.onDidOpenTextDocument)
.through(excludeIrrelevantTextDocumentsByScheme)
.through(excludeIrrelevantTextDocumentsByLanguage);

export const onDidCloseRelevantTextDocument: Event<TextDocument> =
streamEvents(workspace.onDidCloseTextDocument)
EventStream.of(workspace.onDidCloseTextDocument)
.through(excludeIrrelevantTextDocumentsByScheme)
.through(excludeIrrelevantTextDocumentsByLanguage);
7 changes: 2 additions & 5 deletions extension/src/events/filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Disposable,
DocumentSelector,
Event,
languages,
Expand Down Expand Up @@ -74,10 +73,8 @@ export function select<T>(
upstreamEvent: Event<T>,
): Event<T> {
return (
listener: (e: T) => any,
listenerThisArgs?: any,
disposables?: Disposable[],
): Disposable => {
...[listener, listenerThisArgs, disposables]: Parameters<Event<T>>
) => {
const upstreamListener: (e: T) => any = (e) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return match(e) ? listener.call(listenerThisArgs, e) : null;
Expand Down
28 changes: 17 additions & 11 deletions extension/src/events/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@ interface EventStreamFunction<T, U, A extends Arr> {
(...args: [...A, Event<T>]): Event<U>;
}

interface EventStream<T> extends Event<T> {
export class EventStream<T> {
event: Event<T>;

map<U>(fn: (e: Event<T>) => Event<U>): EventStream<U> & Event<U> {
return EventStream.of(fn(this.event));
}

through<U, A extends Arr>(
fn: EventStreamFunction<T, U, A>,
...args: A
): EventStream<U>;
}
): EventStream<U> & Event<U> {
return EventStream.of(fn(...args, this.event));
}

export function streamEvents<T>(event: Event<T>): EventStream<T> {
const stream: EventStream<T> = function (...args) {
return event(...args);
};
stream.through = (fn, ...args) => {
return streamEvents(fn(...args, event));
};
return stream;
static of<T>(event: Event<T>): EventStream<T> & Event<T> {
const result: EventStream<T> & Event<T> = (...args) => event(...args);
result.map = EventStream.prototype.map;
result.through = EventStream.prototype.through;
result.event = event;
return result;
}
}
84 changes: 64 additions & 20 deletions extension/src/events/throttle.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,73 @@
import { Disposable, Event } from "vscode";
import { Event } from "vscode";

import { throttle } from "throttle-debounce";

export function throttleEvent<T, U>(
/*
* 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<U> = Map<U, () => void>;

function _throttleEvent<T, U>(
delayMs: number,
groupBy: (event: T) => U,
upstreamEvent: Event<T>,
): Event<U> {
return (
listener: (eventGroup: U) => any,
listenerThisArgs?: any,
disposables?: Disposable[],
): Disposable => {
const throttledListenersByGroup: Map<U, () => 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<Event<U>>
) {

// Keys: compound payloads
// Values: throttled invocations of compound events
const listenerMap: _ListenerMap<U> = 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<T, U>(delayMs: number, groupBy: (event: T) => U) {
return (upstreamEvent: Event<T>) => {
return (...eventArgs: Parameters<Event<U>>) => {
return _throttleEvent(delayMs, groupBy, upstreamEvent, ...eventArgs);
};
return upstreamEvent(upstreamListener, null, disposables);
};
}

0 comments on commit 9d74786

Please sign in to comment.