Open-Source Wikis

/

Helix

/

Packages

/

helix-event

helix-editor/helix

helix-event

Synchronous and asynchronous event hooks, debouncing, redraw locks, and a global registry. ~1,100 lines of Rust.

Purpose

helix-event is the decoupling glue between subsystems. Without it, every feature that wants to react to a document change (completion, signature help, diagnostics fetch, save) would need direct access to the code that mutates documents. Instead, mutators emit typed events and any number of hooks subscribe.

The crate also provides primitives for async, often-debounced work — completion request debouncing is the canonical use case — plus a redraw signaling channel and a status-message channel.

Directory layout

helix-event/src
├── lib.rs       # public surface; events! and register_hook! re-exports
├── registry.rs  # Event trait (private), per-event hook list, dispatch
├── hook.rs      # the `events!` macro implementation
├── debounce.rs  # AsyncHook trait + send_blocking helper
├── cancel.rs    # cancelable_future, TaskController, TaskHandle
├── redraw.rs    # request_redraw, lock_frame, RenderLockGuard
├── runtime.rs   # tokio runtime helpers (used by tests)
├── status.rs    # status-line message channel
└── test.rs      # internal tests

Key abstractions

Type File Purpose
Event (sealed trait) registry.rs Marker for typed events. Implemented automatically by the events! macro.
AsyncHook (trait) debounce.rs A debounceable async background task. Drives features like completion.
TaskController / TaskHandle cancel.rs Cooperative cancellation primitives.
RenderLockGuard redraw.rs A mutex-like guard that prevents redraws while held; used to batch terminal updates.
events! macro hook.rs Declares an event struct + Event impl.
register_hook! macro lib.rs Safely registers a synchronous hook for an event.
dispatch lib.rs Runs all registered hooks for an event in registration order.

Synchronous hooks

use helix_event::register_hook;

events! {
    DocumentDidChange<'a> {
        doc: &'a mut Document,
        view: ViewId,
        old_text: &'a Rope,
        changes: &'a ChangeSet,
        ghost_transaction: bool,
    }
}

register_hook!(move |event: &mut DocumentDidChange<'_>| {
    // synchronous, runs on whatever thread emitted the event
    Ok(())
});

Hooks run synchronously after dispatch. They can mutate the event payload (note the &mut), so a hook can, for example, hide a popup before the next render. Errors from hooks are logged but don't abort the dispatch chain.

The trade-off: hooks have only the data the emitter passed in. They can't reach into the global Editor state without a long-lived shared reference, which is intentional — that's what AsyncHook is for.

Async hooks

AsyncHook is a trait + a tokio background task:

pub trait AsyncHook: Sync + Send + 'static + Sized {
    type Event: Sync + Send + 'static;
    fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
    fn finish_debounce(&mut self);
    fn spawn(self) -> mpsc::Sender<Self::Event> { /* ... */ }
}

Each event the hook receives can either be processed immediately or shift a deadline. When the deadline expires (and no new event has arrived), finish_debounce is called. Examples:

  • Completion: every PostInsertChar or SelectionDidChange debounces a completion request to the active LSP server. Rapid typing collapses into one request.
  • Signature help: same pattern, lower frequency.
  • Pull-model diagnostics: triggered on document change, batched by language server.

See helix-term/src/handlers/ for production examples.

send_blocking

send_blocking(&tx, event) is the recommended way to push events from a synchronous context (a hook, or a non-async function holding a Sender). It first tries try_send; on Full, it falls back to block_on(tx.send_timeout(_, 10ms)) — the timeout is short enough that even a stuck consumer drops a message rather than freezing the editor.

Redraw and frame locks

The redraw module (redraw.rs) gives any code path:

  • request_redraw() — enqueues a redraw on the next loop iteration.
  • lock_frame() returning RenderLockGuard — keeps the compositor from drawing until released. Used when an async task needs to atomically update multiple pieces of state before they become visible.
  • RequestRedrawOnDrop — a sentinel that issues request_redraw when dropped.

Status messages

helix_event::status::set_status(..) posts a status-line message from any task. The application loop drains the channel and forwards messages to the active editor.

Cancelable tasks

cancel::cancelable_future wraps a future so it terminates when its TaskHandle is dropped or TaskController::cancel is called. This is how completion, document highlights, and signature help cancel stale requests when the user keeps typing.

Integration points

  • helix-view declares document and selection events (helix-view/src/events.rs) and dispatches them from Document::apply and Editor actions.
  • helix-term/src/handlers/ is the main consumer: every handler is an AsyncHook plus a few register_hook! calls.
  • helix-event itself is consumed by helix-core only minimally (status messages from snippet rendering, etc.).

Entry points for modification

  • Adding an event: declare it via events! next to the subsystem that emits it. Dispatch from the place that mutates state. Subscribers register hooks at startup.
  • Adding a debounced background feature: implement AsyncHook, expose its sender via Handlers (helix-view/src/handlers/mod.rs), wire register_hook! calls in helix-term/src/handlers/<your-feature>.rs.

For the surrounding architecture, see overview/architecture.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

helix-event – Helix wiki | Factory