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 testsKey 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
PostInsertCharorSelectionDidChangedebounces 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()returningRenderLockGuard— 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 issuesrequest_redrawwhen 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-viewdeclares document and selection events (helix-view/src/events.rs) and dispatches them fromDocument::applyandEditoractions.helix-term/src/handlers/is the main consumer: every handler is anAsyncHookplus a fewregister_hook!calls.helix-eventitself is consumed byhelix-coreonly 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 viaHandlers(helix-view/src/handlers/mod.rs), wireregister_hook!calls inhelix-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.