From 13ed4f6c4748019787d24c2b686d417b71604242 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 1 Dec 2023 00:03:26 +0100 Subject: Add hook/event system --- helix-event/src/lib.rs | 201 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 3 deletions(-) (limited to 'helix-event/src/lib.rs') diff --git a/helix-event/src/lib.rs b/helix-event/src/lib.rs index 9c082b93..894de5e8 100644 --- a/helix-event/src/lib.rs +++ b/helix-event/src/lib.rs @@ -1,8 +1,203 @@ //! `helix-event` contains systems that allow (often async) communication between -//! different editor components without strongly coupling them. Currently this -//! crate only contains some smaller facilities but the intend is to add more -//! functionality in the future ( like a generic hook system) +//! different editor components without strongly coupling them. Specifically +//! it allows defining synchronous hooks that run when certain editor events +//! occur. +//! +//! The core of the event system are hook callbacks and the [`Event`] trait. A +//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>` +//! that gets called every time an appropriate event is dispatched. The implementation +//! details of the [`Event`] trait are considered private. The [`events`] macro is +//! provided which automatically declares event types. Similarly the `register_hook` +//! macro should be used to (safely) declare event hooks. +//! +//! Hooks run synchronously which can be advantageous since they can modify the +//! current editor state right away (for example to immediately hide the completion +//! popup). However, they can not contain their own state without locking since +//! they only receive immutable references. For handler that want to track state, do +//! expensive background computations or debouncing an [`AsyncHook`] is preferable. +//! Async hooks are based around a channels that receive events specific to +//! that `AsyncHook` (usually an enum). These events can be sent by synchronous +//! hooks. Due to some limitations around tokio channels the [`send_blocking`] +//! function exported in this crate should be used instead of the builtin +//! `blocking_send`. +//! +//! In addition to the core event system, this crate contains some message queues +//! that allow transfer of data back to the main event loop from async hooks and +//! hooks that may not have access to all application data (for example in helix-view). +//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and +//! display status messages ([`status`]). +//! +//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the +//! main loop (including access to the compositor). Ideally that queue will be moved +//! to helix-view in the future if we manage to detach the compositor from its rendering backend. +use anyhow::Result; +pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx}; +pub use debounce::{send_blocking, AsyncHook}; pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard}; +pub use registry::Event; +mod cancel; +mod debounce; +mod hook; mod redraw; +mod registry; +#[doc(hidden)] +pub mod runtime; +pub mod status; + +#[cfg(test)] +mod test; + +pub fn register_event() { + registry::with_mut(|registry| registry.register_event::()) +} + +/// Registers a hook that will be called when an event of type `E` is dispatched. +/// This function should usually not be used directly, use the [`register_hook`] +/// macro instead. +/// +/// +/// # Safety +/// +/// `hook` must be totally generic over all lifetime parameters of `E`. For +/// example if `E` was a known type `Foo<'a, 'b>`, then the correct trait bound +/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)`, but there is no way to +/// express that kind of constraint for a generic type with the Rust type system +/// as of this writing. +pub unsafe fn register_hook_raw( + hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync, +) { + registry::with_mut(|registry| registry.register_hook(hook)) +} + +/// Register a hook solely by event name +pub fn register_dynamic_hook( + hook: impl Fn() -> Result<()> + 'static + Send + Sync, + id: &str, +) -> Result<()> { + registry::with_mut(|reg| reg.register_dynamic_hook(hook, id)) +} + +pub fn dispatch(e: impl Event) { + registry::with(|registry| registry.dispatch(e)); +} + +/// Macro to declare events +/// +/// # Examples +/// +/// ``` no-compile +/// events! { +/// FileWrite(&Path) +/// ViewScrolled{ view: View, new_pos: ViewOffset } +/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangeSet } +/// } +/// +/// fn init() { +/// register_event::(); +/// register_event::(); +/// register_event::(); +/// } +/// +/// fn save(path: &Path, content: &str){ +/// std::fs::write(path, content); +/// dispatch(FileWrite(path)); +/// } +/// ``` +#[macro_export] +macro_rules! events { + ($name: ident<$($lt: lifetime),*> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => { + pub struct $name<$($lt),*> { $(pub $data: $data_ty),* } + unsafe impl<$($lt),*> $crate::Event for $name<$($lt),*> { + const ID: &'static str = stringify!($name); + const LIFETIMES: usize = $crate::events!(@sum $(1, $lt),*); + type Static = $crate::events!(@replace_lt $name, $('static, $lt),*); + } + $crate::events!{ $($rem)* } + }; + ($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => { + pub struct $name { $(pub $data: $data_ty),* } + unsafe impl $crate::Event for $name { + const ID: &'static str = stringify!($name); + const LIFETIMES: usize = 0; + type Static = Self; + } + $crate::events!{ $($rem)* } + }; + () => {}; + (@replace_lt $name: ident, $($lt1: lifetime, $lt2: lifetime),* ) => {$name<$($lt1),*>}; + (@sum $($val: expr, $lt1: lifetime),* ) => {0 $(+ $val)*}; +} + +/// Safely register statically typed event hooks +#[macro_export] +macro_rules! register_hook { + // Safety: this is safe because we fully control the type of the event here and + // ensure all lifetime arguments are fully generic and the correct number of lifetime arguments + // is present + (move |$event:ident: &mut $event_ty: ident<$($lt: lifetime),*>| $body: expr) => { + let val = move |$event: &mut $event_ty<$($lt),*>| $body; + unsafe { + // Lifetimes are a bit of a pain. We want to allow events being + // non-static. Lifetimes don't actually exist at runtime so its + // fine to essentially transmute the lifetimes as long as we can + // prove soundness. The hook must therefore accept any combination + // of lifetimes. In other words fn(&'_ mut Event<'_, '_>) is ok + // but examples like fn(&'_ mut Event<'_, 'static>) or fn<'a>(&'a + // mut Event<'a, 'a>) are not. To make this safe we use a macro to + // forbid the user from specifying lifetimes manually (all lifetimes + // specified are always function generics and passed to the event so + // lifetimes can't be used multiple times and using 'static causes a + // syntax error). + // + // There is one soundness hole tough: Type Aliases allow + // "accidentally" creating these problems. For example: + // + // type Event2 = Event<'static>. + // type Event2<'a> = Event<'a, a>. + // + // These cases can be caught by counting the number of lifetimes + // parameters at the parameter declaration site and then at the hook + // declaration site. By asserting the number of lifetime parameters + // are equal we can catch all bad type aliases under one assumption: + // There are no unused lifetime parameters. Introducing a static + // would reduce the number of arguments of the alias by one in the + // above example Event2 has zero lifetime arguments while the original + // event has one lifetime argument. Similar logic applies to using + // a lifetime argument multiple times. The ASSERT below performs a + // a compile time assertion to ensure exactly this property. + // + // With unused lifetime arguments it is still one way to cause unsound code: + // + // type Event2<'a, 'b> = Event<'a, 'a>; + // + // However, this case will always emit a compiler warning/cause CI + // failures so a user would have to introduce #[allow(unused)] which + // is easily caught in review (and a very theoretical case anyway). + // If we want to be pedantic we can simply compile helix with + // forbid(unused). All of this is just a safety net to prevent + // very theoretical misuse. This won't come up in real code (and is + // easily caught in review). + #[allow(unused)] + const ASSERT: () = { + if <$event_ty as $crate::Event>::LIFETIMES != 0 + $crate::events!(@sum $(1, $lt),*){ + panic!("invalid type alias"); + } + }; + $crate::register_hook_raw::<$crate::events!(@replace_lt $event_ty, $('static, $lt),*)>(val); + } + }; + (move |$event:ident: &mut $event_ty: ident| $body: expr) => { + let val = move |$event: &mut $event_ty| $body; + unsafe { + #[allow(unused)] + const ASSERT: () = { + if <$event_ty as $crate::Event>::LIFETIMES != 0{ + panic!("invalid type alias"); + } + }; + $crate::register_hook_raw::<$event_ty>(val); + } + }; +} -- cgit v1.2.3-70-g09d2