-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
89408cd
commit 9b0fdc7
Showing
26 changed files
with
973 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)* } | ||
}; | ||
() => {}; | ||
} |
Oops, something went wrong.