Skip to content

Commit 9b0fdc7

Browse files
committed
Add hook/event system
1 parent 89408cd commit 9b0fdc7

26 files changed

+973
-50
lines changed

.cargo/config.toml

+14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# we use tokio_unstable to enable runtime::Handle::id so we can seperate
2+
# gloablsfrom mul1tiple parallel tests. If that function ever does get removed
3+
# its possible to replace (with some additional overhead and effort)
4+
# Annoyingly build.rustflags doesn't work here because it gets overwritten
5+
# if people have their own global target.<..> config (for examble to enable mold)
6+
# specificying flags this way is more robust as they get merged
7+
# This still gets overwritten by RUST_FLAGS tough, luckily it shouldn't be necessary
8+
# to set those most of the time. If downstream does overwrite this its not a huge
9+
# deal since it will only break tests anyway
10+
[target."cfg(all())"]
11+
rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"]
12+
13+
114
[alias]
215
xtask = "run --package xtask --"
316
integration-test = "test --features integration --profile integration --workspace --test integration"
17+

Cargo.lock

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helix-event/Cargo.toml

+15-2
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,18 @@ homepage = "https://helix-editor.com"
1111
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1212

1313
[dependencies]
14-
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
15-
parking_lot = { version = "0.12", features = ["send_guard"] }
14+
ahash = "0.8.3"
15+
hashbrown = "0.13.2"
16+
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
17+
# the event registry is essentially read only but must be an rwlock so we can
18+
# setup new events on intalization, hardware-lock-elision hugnly benefits this case
19+
# as is essentially makes the lock entirely free as long as there is no writes
20+
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
21+
once_cell = "1.18"
22+
23+
anyhow = "1"
24+
log = "0.4"
25+
futures-executor = "0.3.28"
26+
27+
[features]
28+
integration_test = []

helix-event/src/cancel.rs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use std::future::Future;
2+
3+
pub use oneshot::channel as cancelation;
4+
use tokio::sync::oneshot;
5+
6+
pub type CancelTx = oneshot::Sender<()>;
7+
pub type CancelRx = oneshot::Receiver<()>;
8+
9+
pub async fn canceable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
10+
tokio::select! {
11+
biased;
12+
_ = cancel => {
13+
None
14+
}
15+
res = future => {
16+
Some(res)
17+
}
18+
}
19+
}

helix-event/src/debounce.rs

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//! Utilities for declaring an async (usually debounced) hook
2+
3+
use std::time::Duration;
4+
5+
use futures_executor::block_on;
6+
use tokio::sync::mpsc::{self, error::TrySendError, Sender};
7+
use tokio::time::Instant;
8+
9+
/// Async hooks are the core of the event system, the m
10+
pub trait AsyncHook: Sync + Send + 'static + Sized {
11+
type Event: Sync + Send + 'static;
12+
/// Called immidietly whenever an event is received, this function can
13+
/// consume the event immidietly or debounce it. In case of debouncing
14+
/// it can either define a new debounce timeout or continue the current
15+
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
16+
17+
/// Called whenever the debounce timeline is searched
18+
fn finish_debounce(&mut self);
19+
20+
fn spawn(self) -> mpsc::Sender<Self::Event> {
21+
// the capaicity doesn't matter too much here, unless the cpu is totally overwhelmed
22+
// the cap will never be reached sine we awalys immidietly drain the channel
23+
// so is should only be reached in case of total CPU overload
24+
// However, a bounded channel is much more efficient so its nice to use here
25+
let (tx, rx) = mpsc::channel(128);
26+
tokio::spawn(run(self, rx));
27+
tx
28+
}
29+
}
30+
31+
async fn run<Hook: AsyncHook>(mut hook: Hook, mut rx: mpsc::Receiver<Hook::Event>) {
32+
let mut deadline = None;
33+
loop {
34+
let event = match deadline {
35+
Some(deadline_) => {
36+
let res = tokio::time::timeout_at(deadline_, rx.recv()).await;
37+
match res {
38+
Ok(event) => event,
39+
Err(_) => {
40+
hook.finish_debounce();
41+
deadline = None;
42+
continue;
43+
}
44+
}
45+
}
46+
None => rx.recv().await,
47+
};
48+
let Some(event) = event else {
49+
break;
50+
};
51+
deadline = hook.handle_event(event, deadline);
52+
}
53+
}
54+
55+
pub fn send_blocking<T>(tx: &Sender<T>, data: T) {
56+
// block_on has some ovherhead and in practice the channel should basically
57+
// never be full anyway so first try sending without blocking
58+
if let Err(TrySendError::Full(data)) = tx.try_send(data) {
59+
// set a timeout so that we just drop a message instead of freezing the editor in the worst case
60+
block_on(tx.send_timeout(data, Duration::from_millis(10))).unwrap();
61+
}
62+
}

helix-event/src/hook.rs

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! rust dynamic dispatch is extremely limited so we have to build our
2+
//! own vtable implementation. Otherwise implementing the event system would not be possible
3+
//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally
4+
//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a
5+
//! pointer to a global (static) vtable entry which itself contains multiple other pointers
6+
//! (the various functions of the trait, drop, size annd align). That makes dynamic
7+
//! dispatch pretty slow (double pointer indirections). However, we only have a single function
8+
//! in the hook trait and don't need adrop implementation (event system is global anyway
9+
//! and never dropped) so we can just store the entire vtable inline.
10+
11+
use anyhow::Result;
12+
use std::ptr::{self, NonNull};
13+
14+
use crate::{Event, Hook};
15+
16+
/// Opaque handle type that represents an erased type parameter.
17+
///
18+
/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but
19+
/// until then we can use this.
20+
///
21+
/// Care should be taken that we don't use a concrete instance of this. It should only be used
22+
/// through a reference, so we can maintain something else's lifetime.
23+
struct Opaque(());
24+
25+
pub(crate) struct ErasedHook {
26+
data: NonNull<Opaque>,
27+
call: unsafe fn(NonNull<Opaque>, NonNull<Opaque>, NonNull<Opaque>),
28+
}
29+
30+
impl ErasedHook {
31+
pub(crate) fn new_dynamic<H: Fn() + Sync + Send + 'static>(hook: H) -> ErasedHook {
32+
unsafe fn call<H: Fn()>(
33+
hook: NonNull<Opaque>,
34+
_event: NonNull<Opaque>,
35+
_result: NonNull<Opaque>,
36+
) {
37+
let hook: NonNull<H> = hook.cast();
38+
let hook: &H = hook.as_ref();
39+
(hook)()
40+
}
41+
42+
unsafe {
43+
ErasedHook {
44+
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
45+
call: call::<H>,
46+
}
47+
}
48+
}
49+
50+
pub(crate) fn new<H: Hook>(hook: H) -> ErasedHook {
51+
unsafe fn call<H: Hook>(
52+
hook: NonNull<Opaque>,
53+
event: NonNull<Opaque>,
54+
result: NonNull<Opaque>,
55+
) {
56+
let hook: NonNull<H> = hook.cast();
57+
let mut event: NonNull<H::Event<'static>> = event.cast();
58+
let result: NonNull<Result<()>> = result.cast();
59+
let res = H::run(hook.as_ref(), event.as_mut());
60+
ptr::write(result.as_ptr(), res)
61+
}
62+
63+
unsafe {
64+
ErasedHook {
65+
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
66+
call: call::<H>,
67+
}
68+
}
69+
}
70+
71+
pub(crate) unsafe fn call<'a, E: Event<'a>>(&self, event: &mut E) -> Result<()> {
72+
let mut res = Ok(());
73+
74+
unsafe {
75+
(self.call)(
76+
self.data,
77+
NonNull::from(event).cast(),
78+
NonNull::from(&mut res).cast(),
79+
);
80+
}
81+
res
82+
}
83+
}
84+
85+
unsafe impl Sync for ErasedHook {}
86+
unsafe impl Send for ErasedHook {}

helix-event/src/lib.rs

+142-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,147 @@
11
//! `helix-event` contains systems that allow (often async) communication between
2-
//! different editor components without strongly coupling them. Currently this
3-
//! crate only contains some smaller facilities but the intend is to add more
4-
//! functionality in the future ( like a generic hook system)
2+
//! different editor components without strongly coupling them. Specifically
3+
//! it allows defining synchronous hooks that run when certain editor events
4+
//! occurs.
5+
//!
6+
//! The core of the event system is the [`Hook`] trait. A hook is essentially
7+
//! just a closure `Fn(event: &mut impl Event) -> Result<()>`. This can currently
8+
//! not be represented in the rust type system with closures (it requires second
9+
//! order generics). Instead we use generic associated types to represent that
10+
//! invariant so a custom type is always required.
11+
//!
12+
//! The [`Event`] trait is unsafe because upon dispatch event lifetimes are
13+
//! essentially erased. To ensure safety all lifetime parameters of the event
14+
//! must oulife the lifetime Parameter of the event trait. To avoid worrying about
15+
//! that (and spreading unsafe everywhere) the [`events`] macro is provided which
16+
//! automatically declares event types.
17+
//!
18+
//! Hooks run synchronously which can be advantageous since they can modify the
19+
//! current editor state right away (for example to immidietly hide the completion
20+
//! popup). However, they can not contain their own state without locking since
21+
//! they only receive immutable references. For handler that want to track state, do
22+
//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
23+
//! Async hooks are based around a channels that receive events specific to
24+
//! that `AsyncHook` (usually an enum). These events can be send by synchronous
25+
//! [`Hook`]s. Due to some limtations around tokio channels the [`send_blocking`]
26+
//! function exported in this crate should be used instead of the builtin
27+
//! `blocking_send`.
28+
//!
29+
//! In addition to the core event system, this crate contains some message queues
30+
//! that allow transfer of data back to the main event loop from async hooks and
31+
//! hooks that may not have access to all application data (for example in helix-view).
32+
//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and
33+
//! display status messages ([`status`]).
34+
//!
35+
//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the
36+
//! main loop (including access to the compositor). Ideally that queue will be moved
37+
//! to helix-view in the future if we manage to detch the comositor from its rendering backgend.
538
39+
use anyhow::Result;
40+
pub use cancel::{canceable_future, cancelation, CancelRx, CancelTx};
41+
pub use debounce::{send_blocking, AsyncHook};
642
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
43+
pub use registry::Event;
744

45+
mod cancel;
46+
mod debounce;
47+
mod hook;
848
mod redraw;
49+
mod registry;
50+
#[doc(hidden)]
51+
pub mod runtime;
52+
pub mod status;
53+
54+
#[cfg(test)]
55+
mod test;
56+
57+
/// A hook is a colsure that will be automatically callen whenever
58+
/// an `Event` of the associated function is [dispatched](crate::dispatch)
59+
/// is called. The closure must be generic over the lifetime of the event.
60+
pub trait Hook: Sized + Sync + Send + 'static {
61+
type Event<'a>: Event<'a>;
62+
fn run(&self, _event: &mut Self::Event<'_>) -> Result<()>;
63+
}
64+
65+
pub fn register_event<E: Event<'static>>() {
66+
registry::with_mut(|registry| registry.register_event::<E>())
67+
}
68+
69+
pub fn register_hook(hook: impl Hook) {
70+
registry::with_mut(|registry| registry.register_hook(hook))
71+
}
72+
73+
pub fn register_dynamic_hook<H: Fn() + Sync + Send + 'static>(hook: H, id: &str) -> Result<()> {
74+
registry::with_mut(|reg| reg.register_dynamic_hook(hook, id))
75+
}
76+
77+
pub fn dispatch<'a>(e: impl Event<'a>) {
78+
registry::with(|registry| registry.dispatch(e));
79+
}
80+
81+
/// Macro to delclare events
82+
///
83+
/// # Examples
84+
///
85+
/// ``` no-compile
86+
/// events! {
87+
/// FileWrite(&Path)
88+
/// ViewScrolled{ view: View, new_pos: ViewOffset }
89+
/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangSet }
90+
/// }
91+
///
92+
/// fn init() {
93+
/// register_event::<FileWrite>();
94+
/// register_event::<ViewScrolled>();
95+
/// register_event::<InsertChar>();
96+
/// register_event::<DocumentChanged>();
97+
/// }
98+
///
99+
/// fn save(path: &Path, content: &str){
100+
/// std::fs::write(path, content);
101+
/// dispach(FilWrite(path));
102+
/// }
103+
/// ```
104+
#[macro_export]
105+
macro_rules! events {
106+
($name: ident($($data: ty),*) $($rem:tt)*) => {
107+
pub struct $name($(pub $data),*);
108+
unsafe impl<'a> $crate::Event<'a> for $name {
109+
const ID: &'static str = stringify!($name);
110+
type Static = Self;
111+
}
112+
$crate::events!{ $($rem)* }
113+
};
114+
($name: ident<$lt: lifetime>($(pub $data: ty),*) $($rem:tt)*) => {
115+
pub struct $name<$lt>($($data),*);
116+
unsafe impl<$lt> $crate::Event<$lt> for $name<$lt> {
117+
const ID: &'static str = stringify!($name);
118+
type Static = $name<'static>;
119+
}
120+
$crate::events!{ $($rem)* }
121+
};
122+
($name: ident<$lt: lifetime> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
123+
pub struct $name<$lt> { $(pub $data: $data_ty),* }
124+
unsafe impl<$lt> $crate::Event<$lt> for $name<$lt> {
125+
const ID: &'static str = stringify!($name);
126+
type Static = $name<'static>;
127+
}
128+
$crate::events!{ $($rem)* }
129+
};
130+
($name: ident<$lt1: lifetime, $lt2: lifetime> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
131+
pub struct $name<$lt1, $lt2> { $(pub $data: $data_ty),* }
132+
unsafe impl<$lt1, $lt2: $lt1> $crate::Event<$lt1> for $name<$lt1, $lt2> {
133+
const ID: &'static str = stringify!($name);
134+
type Static = $name<'static, 'static>;
135+
}
136+
$crate::events!{ $($rem)* }
137+
};
138+
($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
139+
pub struct $name { $(pub $data: $data_ty),* }
140+
unsafe impl<'a> $crate::Event<'a> for $name {
141+
const ID: &'static str = stringify!($name);
142+
type Static = Self;
143+
}
144+
$crate::events!{ $($rem)* }
145+
};
146+
() => {};
147+
}

0 commit comments

Comments
 (0)