Skip to content

Commit

Permalink
Add hook/event system
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalkuthe committed Aug 20, 2023
1 parent 89408cd commit 9b0fdc7
Show file tree
Hide file tree
Showing 26 changed files with 973 additions and 50 deletions.
14 changes: 14 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# we use tokio_unstable to enable runtime::Handle::id so we can seperate
# gloablsfrom mul1tiple parallel tests. If that function ever does get removed
# its possible to replace (with some additional overhead and effort)
# Annoyingly build.rustflags doesn't work here because it gets overwritten
# if people have their own global target.<..> config (for examble to enable mold)
# specificying flags this way is more robust as they get merged
# This still gets overwritten by RUST_FLAGS tough, luckily it shouldn't be necessary
# to set those most of the time. If downstream does overwrite this its not a huge
# deal since it will only break tests anyway
[target."cfg(all())"]
rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"]


[alias]
xtask = "run --package xtask --"
integration-test = "test --features integration --profile integration --workspace --test integration"

15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions helix-event/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,18 @@ homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
parking_lot = { version = "0.12", features = ["send_guard"] }
ahash = "0.8.3"
hashbrown = "0.13.2"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
# the event registry is essentially read only but must be an rwlock so we can
# setup new events on intalization, hardware-lock-elision hugnly benefits this case
# as is essentially makes the lock entirely free as long as there is no writes
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
once_cell = "1.18"

anyhow = "1"
log = "0.4"
futures-executor = "0.3.28"

[features]
integration_test = []
19 changes: 19 additions & 0 deletions helix-event/src/cancel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::future::Future;

pub use oneshot::channel as cancelation;
use tokio::sync::oneshot;

pub type CancelTx = oneshot::Sender<()>;
pub type CancelRx = oneshot::Receiver<()>;

pub async fn canceable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
tokio::select! {
biased;
_ = cancel => {
None
}
res = future => {
Some(res)
}
}
}
62 changes: 62 additions & 0 deletions helix-event/src/debounce.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Utilities for declaring an async (usually debounced) hook
use std::time::Duration;

use futures_executor::block_on;
use tokio::sync::mpsc::{self, error::TrySendError, Sender};
use tokio::time::Instant;

/// Async hooks are the core of the event system, the m
pub trait AsyncHook: Sync + Send + 'static + Sized {
type Event: Sync + Send + 'static;
/// Called immidietly whenever an event is received, this function can
/// consume the event immidietly or debounce it. In case of debouncing
/// it can either define a new debounce timeout or continue the current
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;

/// Called whenever the debounce timeline is searched
fn finish_debounce(&mut self);

fn spawn(self) -> mpsc::Sender<Self::Event> {
// the capaicity doesn't matter too much here, unless the cpu is totally overwhelmed
// the cap will never be reached sine we awalys immidietly drain the channel
// so is should only be reached in case of total CPU overload
// However, a bounded channel is much more efficient so its nice to use here
let (tx, rx) = mpsc::channel(128);
tokio::spawn(run(self, rx));
tx
}
}

async fn run<Hook: AsyncHook>(mut hook: Hook, mut rx: mpsc::Receiver<Hook::Event>) {
let mut deadline = None;
loop {
let event = match deadline {
Some(deadline_) => {
let res = tokio::time::timeout_at(deadline_, rx.recv()).await;
match res {
Ok(event) => event,
Err(_) => {
hook.finish_debounce();
deadline = None;
continue;
}
}
}
None => rx.recv().await,
};
let Some(event) = event else {
break;
};
deadline = hook.handle_event(event, deadline);
}
}

pub fn send_blocking<T>(tx: &Sender<T>, data: T) {
// block_on has some ovherhead and in practice the channel should basically
// never be full anyway so first try sending without blocking
if let Err(TrySendError::Full(data)) = tx.try_send(data) {
// set a timeout so that we just drop a message instead of freezing the editor in the worst case
block_on(tx.send_timeout(data, Duration::from_millis(10))).unwrap();
}
}
86 changes: 86 additions & 0 deletions helix-event/src/hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! rust dynamic dispatch is extremely limited so we have to build our
//! own vtable implementation. Otherwise implementing the event system would not be possible
//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally
//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a
//! pointer to a global (static) vtable entry which itself contains multiple other pointers
//! (the various functions of the trait, drop, size annd align). That makes dynamic
//! dispatch pretty slow (double pointer indirections). However, we only have a single function
//! in the hook trait and don't need adrop implementation (event system is global anyway
//! and never dropped) so we can just store the entire vtable inline.
use anyhow::Result;
use std::ptr::{self, NonNull};

use crate::{Event, Hook};

/// Opaque handle type that represents an erased type parameter.
///
/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but
/// until then we can use this.
///
/// Care should be taken that we don't use a concrete instance of this. It should only be used
/// through a reference, so we can maintain something else's lifetime.
struct Opaque(());

pub(crate) struct ErasedHook {
data: NonNull<Opaque>,
call: unsafe fn(NonNull<Opaque>, NonNull<Opaque>, NonNull<Opaque>),
}

impl ErasedHook {
pub(crate) fn new_dynamic<H: Fn() + Sync + Send + 'static>(hook: H) -> ErasedHook {
unsafe fn call<H: Fn()>(
hook: NonNull<Opaque>,
_event: NonNull<Opaque>,
_result: NonNull<Opaque>,
) {
let hook: NonNull<H> = hook.cast();
let hook: &H = hook.as_ref();
(hook)()
}

unsafe {
ErasedHook {
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
call: call::<H>,
}
}
}

pub(crate) fn new<H: Hook>(hook: H) -> ErasedHook {
unsafe fn call<H: Hook>(
hook: NonNull<Opaque>,
event: NonNull<Opaque>,
result: NonNull<Opaque>,
) {
let hook: NonNull<H> = hook.cast();
let mut event: NonNull<H::Event<'static>> = event.cast();
let result: NonNull<Result<()>> = result.cast();
let res = H::run(hook.as_ref(), event.as_mut());
ptr::write(result.as_ptr(), res)
}

unsafe {
ErasedHook {
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
call: call::<H>,
}
}
}

pub(crate) unsafe fn call<'a, E: Event<'a>>(&self, event: &mut E) -> Result<()> {
let mut res = Ok(());

unsafe {
(self.call)(
self.data,
NonNull::from(event).cast(),
NonNull::from(&mut res).cast(),
);
}
res
}
}

unsafe impl Sync for ErasedHook {}
unsafe impl Send for ErasedHook {}
145 changes: 142 additions & 3 deletions helix-event/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,147 @@
//! `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
//! occurs.
//!
//! The core of the event system is the [`Hook`] trait. A hook is essentially
//! just a closure `Fn(event: &mut impl Event) -> Result<()>`. This can currently
//! not be represented in the rust type system with closures (it requires second
//! order generics). Instead we use generic associated types to represent that
//! invariant so a custom type is always required.
//!
//! The [`Event`] trait is unsafe because upon dispatch event lifetimes are
//! essentially erased. To ensure safety all lifetime parameters of the event
//! must oulife the lifetime Parameter of the event trait. To avoid worrying about
//! that (and spreading unsafe everywhere) the [`events`] macro is provided which
//! automatically declares event types.
//!
//! Hooks run synchronously which can be advantageous since they can modify the
//! current editor state right away (for example to immidietly 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 send by synchronous
//! [`Hook`]s. Due to some limtations 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 detch the comositor from its rendering backgend.
use anyhow::Result;
pub use cancel::{canceable_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;

/// A hook is a colsure that will be automatically callen whenever
/// an `Event` of the associated function is [dispatched](crate::dispatch)
/// is called. The closure must be generic over the lifetime of the event.
pub trait Hook: Sized + Sync + Send + 'static {
type Event<'a>: Event<'a>;
fn run(&self, _event: &mut Self::Event<'_>) -> Result<()>;
}

pub fn register_event<E: Event<'static>>() {
registry::with_mut(|registry| registry.register_event::<E>())
}

pub fn register_hook(hook: impl Hook) {
registry::with_mut(|registry| registry.register_hook(hook))
}

pub fn register_dynamic_hook<H: Fn() + Sync + Send + 'static>(hook: H, id: &str) -> Result<()> {
registry::with_mut(|reg| reg.register_dynamic_hook(hook, id))
}

pub fn dispatch<'a>(e: impl Event<'a>) {
registry::with(|registry| registry.dispatch(e));
}

/// Macro to delclare 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 ChangSet }
/// }
///
/// fn init() {
/// register_event::<FileWrite>();
/// register_event::<ViewScrolled>();
/// register_event::<InsertChar>();
/// register_event::<DocumentChanged>();
/// }
///
/// fn save(path: &Path, content: &str){
/// std::fs::write(path, content);
/// dispach(FilWrite(path));
/// }
/// ```
#[macro_export]
macro_rules! events {
($name: ident($($data: ty),*) $($rem:tt)*) => {
pub struct $name($(pub $data),*);
unsafe impl<'a> $crate::Event<'a> for $name {
const ID: &'static str = stringify!($name);
type Static = Self;
}
$crate::events!{ $($rem)* }
};
($name: ident<$lt: lifetime>($(pub $data: ty),*) $($rem:tt)*) => {
pub struct $name<$lt>($($data),*);
unsafe impl<$lt> $crate::Event<$lt> for $name<$lt> {
const ID: &'static str = stringify!($name);
type Static = $name<'static>;
}
$crate::events!{ $($rem)* }
};
($name: ident<$lt: lifetime> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name<$lt> { $(pub $data: $data_ty),* }
unsafe impl<$lt> $crate::Event<$lt> for $name<$lt> {
const ID: &'static str = stringify!($name);
type Static = $name<'static>;
}
$crate::events!{ $($rem)* }
};
($name: ident<$lt1: lifetime, $lt2: lifetime> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name<$lt1, $lt2> { $(pub $data: $data_ty),* }
unsafe impl<$lt1, $lt2: $lt1> $crate::Event<$lt1> for $name<$lt1, $lt2> {
const ID: &'static str = stringify!($name);
type Static = $name<'static, 'static>;
}
$crate::events!{ $($rem)* }
};
($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name { $(pub $data: $data_ty),* }
unsafe impl<'a> $crate::Event<'a> for $name {
const ID: &'static str = stringify!($name);
type Static = Self;
}
$crate::events!{ $($rem)* }
};
() => {};
}
Loading

0 comments on commit 9b0fdc7

Please sign in to comment.