Add hook/event system
This commit is contained in:
parent
7d7ace551c
commit
13ed4f6c47
26 changed files with 1023 additions and 49 deletions
|
@ -1,3 +1,17 @@
|
||||||
|
# we use tokio_unstable to enable runtime::Handle::id so we can separate
|
||||||
|
# globals from multiple 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 example to enable mold)
|
||||||
|
# specifying flags this way is more robust as they get merged
|
||||||
|
# This still gets overwritten by RUST_FLAGS though, 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]
|
[alias]
|
||||||
xtask = "run --package xtask --"
|
xtask = "run --package xtask --"
|
||||||
integration-test = "test --features integration --profile integration --workspace --test integration"
|
integration-test = "test --features integration --profile integration --workspace --test integration"
|
||||||
|
|
||||||
|
|
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -1102,6 +1102,12 @@ dependencies = [
|
||||||
name = "helix-event"
|
name = "helix-event"
|
||||||
version = "23.10.0"
|
version = "23.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"anyhow",
|
||||||
|
"futures-executor",
|
||||||
|
"hashbrown 0.14.3",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,5 +12,18 @@ homepage.workspace = true
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
|
ahash = "0.8.3"
|
||||||
parking_lot = { version = "0.12", features = ["send_guard"] }
|
hashbrown = "0.14.0"
|
||||||
|
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 initialization, hardware-lock-elision hugely benefits this case
|
||||||
|
# as it 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
helix-event/src/cancel.rs
Normal file
19
helix-event/src/cancel.rs
Normal file
|
@ -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 cancelable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
|
||||||
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = cancel => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
res = future => {
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
helix-event/src/debounce.rs
Normal file
67
helix-event/src/debounce.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
//! 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 provide a convenient framework for implementing (debounced)
|
||||||
|
/// async event handlers. Most synchronous event hooks will likely need to
|
||||||
|
/// debounce their events, coordinate multiple different hooks and potentially
|
||||||
|
/// track some state. `AsyncHooks` facilitate these use cases by running as
|
||||||
|
/// a background tokio task that waits for events (usually an enum) to be
|
||||||
|
/// sent through a channel.
|
||||||
|
pub trait AsyncHook: Sync + Send + 'static + Sized {
|
||||||
|
type Event: Sync + Send + 'static;
|
||||||
|
/// Called immediately whenever an event is received, this function can
|
||||||
|
/// consume the event immediately or debounce it. In case of debouncing,
|
||||||
|
/// it can either define a new debounce timeout or continue the current one
|
||||||
|
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
|
||||||
|
|
||||||
|
/// Called whenever the debounce timeline is reached
|
||||||
|
fn finish_debounce(&mut self);
|
||||||
|
|
||||||
|
fn spawn(self) -> mpsc::Sender<Self::Event> {
|
||||||
|
// the capacity doesn't matter too much here, unless the cpu is totally overwhelmed
|
||||||
|
// the cap will never be reached since we always immediately drain the channel
|
||||||
|
// so it should only be reached in case of total CPU overload.
|
||||||
|
// However, a bounded channel is much more efficient so it's 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 overhead 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
|
||||||
|
let _ = block_on(tx.send_timeout(data, Duration::from_millis(10)));
|
||||||
|
}
|
||||||
|
}
|
91
helix-event/src/hook.rs
Normal file
91
helix-event/src/hook.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
//! 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 and 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 a drop 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;
|
||||||
|
|
||||||
|
/// 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() -> Result<()> + 'static + Send + Sync>(
|
||||||
|
hook: H,
|
||||||
|
) -> ErasedHook {
|
||||||
|
unsafe fn call<F: Fn() -> Result<()> + 'static + Send + Sync>(
|
||||||
|
hook: NonNull<Opaque>,
|
||||||
|
_event: NonNull<Opaque>,
|
||||||
|
result: NonNull<Opaque>,
|
||||||
|
) {
|
||||||
|
let hook: NonNull<F> = hook.cast();
|
||||||
|
let result: NonNull<Result<()>> = result.cast();
|
||||||
|
let hook: &F = hook.as_ref();
|
||||||
|
let res = hook();
|
||||||
|
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) fn new<E: Event, F: Fn(&mut E) -> Result<()>>(hook: F) -> ErasedHook {
|
||||||
|
unsafe fn call<E: Event, F: Fn(&mut E) -> Result<()>>(
|
||||||
|
hook: NonNull<Opaque>,
|
||||||
|
event: NonNull<Opaque>,
|
||||||
|
result: NonNull<Opaque>,
|
||||||
|
) {
|
||||||
|
let hook: NonNull<F> = hook.cast();
|
||||||
|
let mut event: NonNull<E> = event.cast();
|
||||||
|
let result: NonNull<Result<()>> = result.cast();
|
||||||
|
let hook: &F = hook.as_ref();
|
||||||
|
let res = hook(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::<E, F>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) unsafe fn call<E: Event>(&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 {}
|
|
@ -1,8 +1,203 @@
|
||||||
//! `helix-event` contains systems that allow (often async) communication between
|
//! `helix-event` contains systems that allow (often async) communication between
|
||||||
//! different editor components without strongly coupling them. Currently this
|
//! different editor components without strongly coupling them. Specifically
|
||||||
//! crate only contains some smaller facilities but the intend is to add more
|
//! it allows defining synchronous hooks that run when certain editor events
|
||||||
//! functionality in the future ( like a generic hook system)
|
//! 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 redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
|
||||||
|
pub use registry::Event;
|
||||||
|
|
||||||
|
mod cancel;
|
||||||
|
mod debounce;
|
||||||
|
mod hook;
|
||||||
mod redraw;
|
mod redraw;
|
||||||
|
mod registry;
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod runtime;
|
||||||
|
pub mod status;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
pub fn register_event<E: Event + 'static>() {
|
||||||
|
registry::with_mut(|registry| registry.register_event::<E>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<E: Event>(
|
||||||
|
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::<FileWrite>();
|
||||||
|
/// register_event::<ViewScrolled>();
|
||||||
|
/// register_event::<DocumentChanged>();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -5,16 +5,20 @@ use std::future::Future;
|
||||||
use parking_lot::{RwLock, RwLockReadGuard};
|
use parking_lot::{RwLock, RwLockReadGuard};
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
/// A `Notify` instance that can be used to (asynchronously) request
|
use crate::runtime_local;
|
||||||
/// the editor the render a new frame.
|
|
||||||
static REDRAW_NOTIFY: Notify = Notify::const_new();
|
|
||||||
|
|
||||||
/// A `RwLock` that prevents the next frame from being
|
runtime_local! {
|
||||||
/// drawn until an exclusive (write) lock can be acquired.
|
/// A `Notify` instance that can be used to (asynchronously) request
|
||||||
/// This allows asynchsonous tasks to acquire `non-exclusive`
|
/// the editor to render a new frame.
|
||||||
/// locks (read) to prevent the next frame from being drawn
|
static REDRAW_NOTIFY: Notify = Notify::const_new();
|
||||||
/// until a certain computation has finished.
|
|
||||||
static RENDER_LOCK: RwLock<()> = RwLock::new(());
|
/// A `RwLock` that prevents the next frame from being
|
||||||
|
/// drawn until an exclusive (write) lock can be acquired.
|
||||||
|
/// This allows asynchronous tasks to acquire `non-exclusive`
|
||||||
|
/// locks (read) to prevent the next frame from being drawn
|
||||||
|
/// until a certain computation has finished.
|
||||||
|
static RENDER_LOCK: RwLock<()> = RwLock::new(());
|
||||||
|
}
|
||||||
|
|
||||||
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
|
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
|
||||||
|
|
||||||
|
|
131
helix-event/src/registry.rs
Normal file
131
helix-event/src/registry.rs
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
//! A global registry where events are registered and can be
|
||||||
|
//! subscribed to by registering hooks. The registry identifies event
|
||||||
|
//! types using their type name so multiple event with the same type name
|
||||||
|
//! may not be registered (will cause a panic to ensure soundness)
|
||||||
|
|
||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use hashbrown::hash_map::Entry;
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
use crate::hook::ErasedHook;
|
||||||
|
use crate::runtime_local;
|
||||||
|
|
||||||
|
pub struct Registry {
|
||||||
|
events: HashMap<&'static str, TypeId, ahash::RandomState>,
|
||||||
|
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Registry {
|
||||||
|
pub fn register_event<E: Event + 'static>(&mut self) {
|
||||||
|
let ty = TypeId::of::<E>();
|
||||||
|
assert_eq!(ty, TypeId::of::<E::Static>());
|
||||||
|
match self.events.entry(E::ID) {
|
||||||
|
Entry::Occupied(entry) => {
|
||||||
|
if entry.get() == &ty {
|
||||||
|
// don't warn during tests to avoid log spam
|
||||||
|
#[cfg(not(feature = "integration_test"))]
|
||||||
|
panic!("Event {} was registered multiple times", E::ID);
|
||||||
|
} else {
|
||||||
|
panic!("Multiple events with ID {} were registered", E::ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(ent) => {
|
||||||
|
ent.insert(ty);
|
||||||
|
self.handlers.insert(E::ID, Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # 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
|
||||||
|
/// right now.
|
||||||
|
pub unsafe fn register_hook<E: Event>(
|
||||||
|
&mut self,
|
||||||
|
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
|
||||||
|
) {
|
||||||
|
// ensure event type ids match so we can rely on them always matching
|
||||||
|
let id = E::ID;
|
||||||
|
let Some(&event_id) = self.events.get(id) else {
|
||||||
|
panic!("Tried to register handler for unknown event {id}");
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
TypeId::of::<E::Static>() == event_id,
|
||||||
|
"Tried to register invalid hook for event {id}"
|
||||||
|
);
|
||||||
|
let hook = ErasedHook::new(hook);
|
||||||
|
self.handlers.get_mut(id).unwrap().push(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_dynamic_hook(
|
||||||
|
&mut self,
|
||||||
|
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
// ensure event type ids match so we can rely on them always matching
|
||||||
|
if self.events.get(id).is_none() {
|
||||||
|
bail!("Tried to register handler for unknown event {id}");
|
||||||
|
};
|
||||||
|
let hook = ErasedHook::new_dynamic(hook);
|
||||||
|
self.handlers.get_mut(id).unwrap().push(hook);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch<E: Event>(&self, mut event: E) {
|
||||||
|
let Some(hooks) = self.handlers.get(E::ID) else {
|
||||||
|
log::error!("Dispatched unknown event {}", E::ID);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let event_id = self.events[E::ID];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
TypeId::of::<E::Static>(),
|
||||||
|
event_id,
|
||||||
|
"Tried to dispatch invalid event {}",
|
||||||
|
E::ID
|
||||||
|
);
|
||||||
|
|
||||||
|
for hook in hooks {
|
||||||
|
// safety: event type is the same
|
||||||
|
if let Err(err) = unsafe { hook.call(&mut event) } {
|
||||||
|
log::error!("{} hook failed: {err:#?}", E::ID);
|
||||||
|
crate::status::report_blocking(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_local! {
|
||||||
|
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
|
||||||
|
// hardcoded random number is good enough here we don't care about DOS resistance
|
||||||
|
// and avoids the additional complexity of `Option<Registry>`
|
||||||
|
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
|
||||||
|
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with<T>(f: impl FnOnce(&Registry) -> T) -> T {
|
||||||
|
f(®ISTRY.read())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_mut<T>(f: impl FnOnce(&mut Registry) -> T) -> T {
|
||||||
|
f(&mut REGISTRY.write())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// The number of specified lifetimes and the static type *must* be correct.
|
||||||
|
/// This is ensured automatically by the [`events`](crate::events)
|
||||||
|
/// macro.
|
||||||
|
pub unsafe trait Event: Sized {
|
||||||
|
/// Globally unique (case sensitive) string that identifies this type.
|
||||||
|
/// A good candidate is the events type name
|
||||||
|
const ID: &'static str;
|
||||||
|
const LIFETIMES: usize;
|
||||||
|
type Static: Event + 'static;
|
||||||
|
}
|
88
helix-event/src/runtime.rs
Normal file
88
helix-event/src/runtime.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
//! The event system makes use of global to decouple different systems.
|
||||||
|
//! However, this can cause problems for the integration test system because
|
||||||
|
//! it runs multiple helix applications in parallel. Making the globals
|
||||||
|
//! thread-local does not work because a applications can/does have multiple
|
||||||
|
//! runtime threads. Instead this crate implements a similar notion to a thread
|
||||||
|
//! local but instead of being local to a single thread, the statics are local to
|
||||||
|
//! a single tokio-runtime. The implementation requires locking so it's not exactly efficient.
|
||||||
|
//!
|
||||||
|
//! Therefore this function is only enabled during integration tests and behaves like
|
||||||
|
//! a normal static otherwise. I would prefer this module to be fully private and to only
|
||||||
|
//! export the macro but the macro still need to construct these internals so it's marked
|
||||||
|
//! `doc(hidden)` instead
|
||||||
|
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "integration_test"))]
|
||||||
|
pub struct RuntimeLocal<T: 'static> {
|
||||||
|
/// inner API used in the macro, not part of public API
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub __data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "integration_test"))]
|
||||||
|
impl<T> Deref for RuntimeLocal<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.__data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "integration_test"))]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! runtime_local {
|
||||||
|
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
|
||||||
|
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal {
|
||||||
|
__data: $init
|
||||||
|
};)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "integration_test")]
|
||||||
|
pub struct RuntimeLocal<T: 'static> {
|
||||||
|
data:
|
||||||
|
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
|
||||||
|
init: fn() -> T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "integration_test")]
|
||||||
|
impl<T> RuntimeLocal<T> {
|
||||||
|
/// inner API used in the macro, not part of public API
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub const fn __new(init: fn() -> T) -> Self {
|
||||||
|
Self {
|
||||||
|
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
|
||||||
|
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
|
||||||
|
)),
|
||||||
|
init,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "integration_test")]
|
||||||
|
impl<T> Deref for RuntimeLocal<T> {
|
||||||
|
type Target = T;
|
||||||
|
fn deref(&self) -> &T {
|
||||||
|
let id = tokio::runtime::Handle::current().id();
|
||||||
|
let guard = self.data.read();
|
||||||
|
match guard.get(&id) {
|
||||||
|
Some(res) => res,
|
||||||
|
None => {
|
||||||
|
drop(guard);
|
||||||
|
let data = Box::leak(Box::new((self.init)()));
|
||||||
|
let mut guard = self.data.write();
|
||||||
|
guard.insert(id, data);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "integration_test")]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! runtime_local {
|
||||||
|
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
|
||||||
|
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal::__new(|| $init);)*
|
||||||
|
};
|
||||||
|
}
|
68
helix-event/src/status.rs
Normal file
68
helix-event/src/status.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
//! A queue of async messages/errors that will be shown in the editor
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::{runtime_local, send_blocking};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
|
/// Describes the severity level of a [`StatusMessage`].
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub enum Severity {
|
||||||
|
Hint,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StatusMessage {
|
||||||
|
pub severity: Severity,
|
||||||
|
pub message: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for StatusMessage {
|
||||||
|
fn from(err: anyhow::Error) -> Self {
|
||||||
|
StatusMessage {
|
||||||
|
severity: Severity::Error,
|
||||||
|
message: err.to_string().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for StatusMessage {
|
||||||
|
fn from(msg: &'static str) -> Self {
|
||||||
|
StatusMessage {
|
||||||
|
severity: Severity::Info,
|
||||||
|
message: msg.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_local! {
|
||||||
|
static MESSAGES: OnceCell<Sender<StatusMessage>> = OnceCell::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn report(msg: impl Into<StatusMessage>) {
|
||||||
|
// if the error channel overflows just ignore it
|
||||||
|
let _ = MESSAGES
|
||||||
|
.wait()
|
||||||
|
.send_timeout(msg.into(), Duration::from_millis(10))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_blocking(msg: impl Into<StatusMessage>) {
|
||||||
|
let messages = MESSAGES.wait();
|
||||||
|
send_blocking(messages, msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Must be called once during editor startup exactly once
|
||||||
|
/// before any of the messages in this module can be used
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If called multiple times
|
||||||
|
pub fn setup() -> Receiver<StatusMessage> {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(128);
|
||||||
|
let _ = MESSAGES.set(tx);
|
||||||
|
rx
|
||||||
|
}
|
90
helix-event/src/test.rs
Normal file
90
helix-event/src/test.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
use crate::{dispatch, events, register_dynamic_hook, register_event, register_hook};
|
||||||
|
#[test]
|
||||||
|
fn smoke_test() {
|
||||||
|
events! {
|
||||||
|
Event1 { content: String }
|
||||||
|
Event2 { content: usize }
|
||||||
|
}
|
||||||
|
register_event::<Event1>();
|
||||||
|
register_event::<Event2>();
|
||||||
|
|
||||||
|
// setup hooks
|
||||||
|
let res1: Arc<Mutex<String>> = Arc::default();
|
||||||
|
let acc = Arc::clone(&res1);
|
||||||
|
register_hook!(move |event: &mut Event1| {
|
||||||
|
acc.lock().push_str(&event.content);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
let res2: Arc<AtomicUsize> = Arc::default();
|
||||||
|
let acc = Arc::clone(&res2);
|
||||||
|
register_hook!(move |event: &mut Event2| {
|
||||||
|
acc.fetch_add(event.content, Ordering::Relaxed);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
// triggers events
|
||||||
|
let thread = std::thread::spawn(|| {
|
||||||
|
for i in 0..1000 {
|
||||||
|
dispatch(Event2 { content: i });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
dispatch(Event1 {
|
||||||
|
content: "foo".to_owned(),
|
||||||
|
});
|
||||||
|
dispatch(Event2 { content: 42 });
|
||||||
|
dispatch(Event1 {
|
||||||
|
content: "bar".to_owned(),
|
||||||
|
});
|
||||||
|
dispatch(Event1 {
|
||||||
|
content: "hello world".to_owned(),
|
||||||
|
});
|
||||||
|
thread.join().unwrap();
|
||||||
|
|
||||||
|
// check output
|
||||||
|
assert_eq!(&**res1.lock(), "foobarhello world");
|
||||||
|
assert_eq!(
|
||||||
|
res2.load(Ordering::Relaxed),
|
||||||
|
42 + (0..1000usize).sum::<usize>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic() {
|
||||||
|
events! {
|
||||||
|
Event3 {}
|
||||||
|
Event4 { count: usize }
|
||||||
|
};
|
||||||
|
register_event::<Event3>();
|
||||||
|
register_event::<Event4>();
|
||||||
|
|
||||||
|
let count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let count1 = count.clone();
|
||||||
|
let count2 = count.clone();
|
||||||
|
register_dynamic_hook(
|
||||||
|
move || {
|
||||||
|
count1.fetch_add(2, Ordering::Relaxed);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
"Event3",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
register_dynamic_hook(
|
||||||
|
move || {
|
||||||
|
count2.fetch_add(3, Ordering::Relaxed);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
"Event4",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
dispatch(Event3 {});
|
||||||
|
dispatch(Event4 { count: 0 });
|
||||||
|
dispatch(Event3 {});
|
||||||
|
assert_eq!(count.load(Ordering::Relaxed), 7)
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ homepage.workspace = true
|
||||||
[features]
|
[features]
|
||||||
default = ["git"]
|
default = ["git"]
|
||||||
unicode-lines = ["helix-core/unicode-lines"]
|
unicode-lines = ["helix-core/unicode-lines"]
|
||||||
integration = []
|
integration = ["helix-event/integration_test"]
|
||||||
git = ["helix-vcs/git"]
|
git = ["helix-vcs/git"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
use arc_swap::{access::Map, ArcSwap};
|
use arc_swap::{access::Map, ArcSwap};
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use helix_core::{pos_at_coords, syntax, Selection};
|
use helix_core::{
|
||||||
|
chars::char_is_word,
|
||||||
|
diagnostic::{DiagnosticTag, NumberOrString},
|
||||||
|
pos_at_coords, syntax, Selection,
|
||||||
|
};
|
||||||
use helix_lsp::{
|
use helix_lsp::{
|
||||||
lsp::{self, notification::Notification},
|
lsp::{self, notification::Notification},
|
||||||
util::lsp_range_to_range,
|
util::lsp_range_to_range,
|
||||||
|
@ -24,6 +28,7 @@ use crate::{
|
||||||
commands::apply_workspace_edit,
|
commands::apply_workspace_edit,
|
||||||
compositor::{Compositor, Event},
|
compositor::{Compositor, Event},
|
||||||
config::Config,
|
config::Config,
|
||||||
|
handlers,
|
||||||
job::Jobs,
|
job::Jobs,
|
||||||
keymap::Keymaps,
|
keymap::Keymaps,
|
||||||
ui::{self, overlay::overlaid},
|
ui::{self, overlay::overlaid},
|
||||||
|
@ -138,6 +143,7 @@ impl Application {
|
||||||
let area = terminal.size().expect("couldn't get terminal size");
|
let area = terminal.size().expect("couldn't get terminal size");
|
||||||
let mut compositor = Compositor::new(area);
|
let mut compositor = Compositor::new(area);
|
||||||
let config = Arc::new(ArcSwap::from_pointee(config));
|
let config = Arc::new(ArcSwap::from_pointee(config));
|
||||||
|
let handlers = handlers::setup(config.clone());
|
||||||
let mut editor = Editor::new(
|
let mut editor = Editor::new(
|
||||||
area,
|
area,
|
||||||
theme_loader.clone(),
|
theme_loader.clone(),
|
||||||
|
@ -145,6 +151,7 @@ impl Application {
|
||||||
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
|
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||||
&config.editor
|
&config.editor
|
||||||
})),
|
})),
|
||||||
|
handlers,
|
||||||
);
|
);
|
||||||
|
|
||||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||||
|
@ -321,10 +328,21 @@ impl Application {
|
||||||
Some(event) = input_stream.next() => {
|
Some(event) = input_stream.next() => {
|
||||||
self.handle_terminal_events(event).await;
|
self.handle_terminal_events(event).await;
|
||||||
}
|
}
|
||||||
Some(callback) = self.jobs.futures.next() => {
|
Some(callback) = self.jobs.callbacks.recv() => {
|
||||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
|
||||||
self.render().await;
|
self.render().await;
|
||||||
}
|
}
|
||||||
|
Some(msg) = self.jobs.status_messages.recv() => {
|
||||||
|
let severity = match msg.severity{
|
||||||
|
helix_event::status::Severity::Hint => Severity::Hint,
|
||||||
|
helix_event::status::Severity::Info => Severity::Info,
|
||||||
|
helix_event::status::Severity::Warning => Severity::Warning,
|
||||||
|
helix_event::status::Severity::Error => Severity::Error,
|
||||||
|
};
|
||||||
|
// TODO: show multiple status messages at once to avoid clobbering
|
||||||
|
self.editor.status_msg = Some((msg.message, severity));
|
||||||
|
helix_event::request_redraw();
|
||||||
|
}
|
||||||
Some(callback) = self.jobs.wait_futures.next() => {
|
Some(callback) = self.jobs.wait_futures.next() => {
|
||||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||||
self.render().await;
|
self.render().await;
|
||||||
|
|
|
@ -88,7 +88,7 @@ pub struct Context<'a> {
|
||||||
pub count: Option<NonZeroUsize>,
|
pub count: Option<NonZeroUsize>,
|
||||||
pub editor: &'a mut Editor,
|
pub editor: &'a mut Editor,
|
||||||
|
|
||||||
pub callback: Option<crate::compositor::Callback>,
|
pub callback: Vec<crate::compositor::Callback>,
|
||||||
pub on_next_key_callback: Option<OnKeyCallback>,
|
pub on_next_key_callback: Option<OnKeyCallback>,
|
||||||
pub jobs: &'a mut Jobs,
|
pub jobs: &'a mut Jobs,
|
||||||
}
|
}
|
||||||
|
@ -96,16 +96,18 @@ pub struct Context<'a> {
|
||||||
impl<'a> Context<'a> {
|
impl<'a> Context<'a> {
|
||||||
/// Push a new component onto the compositor.
|
/// Push a new component onto the compositor.
|
||||||
pub fn push_layer(&mut self, component: Box<dyn Component>) {
|
pub fn push_layer(&mut self, component: Box<dyn Component>) {
|
||||||
self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
|
self.callback
|
||||||
compositor.push(component)
|
.push(Box::new(|compositor: &mut Compositor, _| {
|
||||||
}));
|
compositor.push(component)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call `replace_or_push` on the Compositor
|
/// Call `replace_or_push` on the Compositor
|
||||||
pub fn replace_or_push_layer<T: Component>(&mut self, id: &'static str, component: T) {
|
pub fn replace_or_push_layer<T: Component>(&mut self, id: &'static str, component: T) {
|
||||||
self.callback = Some(Box::new(move |compositor: &mut Compositor, _| {
|
self.callback
|
||||||
compositor.replace_or_push(id, component);
|
.push(Box::new(move |compositor: &mut Compositor, _| {
|
||||||
}));
|
compositor.replace_or_push(id, component);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -2934,7 +2936,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||||
let register = cx.register;
|
let register = cx.register;
|
||||||
let count = cx.count;
|
let count = cx.count;
|
||||||
|
|
||||||
cx.callback = Some(Box::new(
|
cx.callback.push(Box::new(
|
||||||
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
||||||
let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
|
let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
|
||||||
[&cx.editor.mode]
|
[&cx.editor.mode]
|
||||||
|
@ -2954,7 +2956,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||||
register,
|
register,
|
||||||
count,
|
count,
|
||||||
editor: cx.editor,
|
editor: cx.editor,
|
||||||
callback: None,
|
callback: Vec::new(),
|
||||||
on_next_key_callback: None,
|
on_next_key_callback: None,
|
||||||
jobs: cx.jobs,
|
jobs: cx.jobs,
|
||||||
};
|
};
|
||||||
|
@ -2982,7 +2984,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||||
|
|
||||||
fn last_picker(cx: &mut Context) {
|
fn last_picker(cx: &mut Context) {
|
||||||
// TODO: last picker does not seem to work well with buffer_picker
|
// TODO: last picker does not seem to work well with buffer_picker
|
||||||
cx.callback = Some(Box::new(|compositor, cx| {
|
cx.callback.push(Box::new(|compositor, cx| {
|
||||||
if let Some(picker) = compositor.last_picker.take() {
|
if let Some(picker) = compositor.last_picker.take() {
|
||||||
compositor.push(picker);
|
compositor.push(picker);
|
||||||
} else {
|
} else {
|
||||||
|
@ -3494,6 +3496,7 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod insert {
|
pub mod insert {
|
||||||
|
use crate::events::PostInsertChar;
|
||||||
use super::*;
|
use super::*;
|
||||||
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
||||||
pub type PostHook = fn(&mut Context, char);
|
pub type PostHook = fn(&mut Context, char);
|
||||||
|
@ -3627,6 +3630,7 @@ pub mod insert {
|
||||||
for hook in &[language_server_completion, signature_help] {
|
for hook in &[language_server_completion, signature_help] {
|
||||||
hook(cx, c);
|
hook(cx, c);
|
||||||
}
|
}
|
||||||
|
helix_event::dispatch(PostInsertChar { c, cx });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn smart_tab(cx: &mut Context) {
|
pub fn smart_tab(cx: &mut Context) {
|
||||||
|
@ -5820,7 +5824,7 @@ fn replay_macro(cx: &mut Context) {
|
||||||
cx.editor.macro_replaying.push(reg);
|
cx.editor.macro_replaying.push(reg);
|
||||||
|
|
||||||
let count = cx.count();
|
let count = cx.count();
|
||||||
cx.callback = Some(Box::new(move |compositor, cx| {
|
cx.callback.push(Box::new(move |compositor, cx| {
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
for &key in keys.iter() {
|
for &key in keys.iter() {
|
||||||
compositor.handle_event(&compositor::Event::Key(key), cx);
|
compositor.handle_event(&compositor::Event::Key(key), cx);
|
||||||
|
|
20
helix-term/src/events.rs
Normal file
20
helix-term/src/events.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use helix_event::{events, register_event};
|
||||||
|
use helix_view::document::Mode;
|
||||||
|
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||||
|
|
||||||
|
use crate::commands;
|
||||||
|
use crate::keymap::MappableCommand;
|
||||||
|
|
||||||
|
events! {
|
||||||
|
OnModeSwitch<'a, 'cx> { old_mode: Mode, new_mode: Mode, cx: &'a mut commands::Context<'cx> }
|
||||||
|
PostInsertChar<'a, 'cx> { c: char, cx: &'a mut commands::Context<'cx> }
|
||||||
|
PostCommand<'a, 'cx> { command: & 'a MappableCommand, cx: &'a mut commands::Context<'cx> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register() {
|
||||||
|
register_event::<OnModeSwitch>();
|
||||||
|
register_event::<PostInsertChar>();
|
||||||
|
register_event::<PostCommand>();
|
||||||
|
register_event::<DocumentDidChange>();
|
||||||
|
register_event::<SelectionDidChange>();
|
||||||
|
}
|
15
helix-term/src/handlers.rs
Normal file
15
helix-term/src/handlers.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::events;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||||
|
events::register();
|
||||||
|
let handlers = Handlers {
|
||||||
|
};
|
||||||
|
handlers
|
||||||
|
}
|
|
@ -1,13 +1,37 @@
|
||||||
|
use helix_event::status::StatusMessage;
|
||||||
|
use helix_event::{runtime_local, send_blocking};
|
||||||
use helix_view::Editor;
|
use helix_view::Editor;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
use crate::compositor::Compositor;
|
use crate::compositor::Compositor;
|
||||||
|
|
||||||
use futures_util::future::{BoxFuture, Future, FutureExt};
|
use futures_util::future::{BoxFuture, Future, FutureExt};
|
||||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||||
|
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||||
|
|
||||||
pub type EditorCompositorCallback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
|
pub type EditorCompositorCallback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
|
||||||
pub type EditorCallback = Box<dyn FnOnce(&mut Editor) + Send>;
|
pub type EditorCallback = Box<dyn FnOnce(&mut Editor) + Send>;
|
||||||
|
|
||||||
|
runtime_local! {
|
||||||
|
static JOB_QUEUE: OnceCell<Sender<Callback>> = OnceCell::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dispatch_callback(job: Callback) {
|
||||||
|
let _ = JOB_QUEUE.wait().send(job).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dispatch(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
|
||||||
|
let _ = JOB_QUEUE
|
||||||
|
.wait()
|
||||||
|
.send(Callback::EditorCompositor(Box::new(job)))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + 'static) {
|
||||||
|
let jobs = JOB_QUEUE.wait();
|
||||||
|
send_blocking(jobs, Callback::EditorCompositor(Box::new(job)))
|
||||||
|
}
|
||||||
|
|
||||||
pub enum Callback {
|
pub enum Callback {
|
||||||
EditorCompositor(EditorCompositorCallback),
|
EditorCompositor(EditorCompositorCallback),
|
||||||
Editor(EditorCallback),
|
Editor(EditorCallback),
|
||||||
|
@ -21,11 +45,11 @@ pub struct Job {
|
||||||
pub wait: bool,
|
pub wait: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Jobs {
|
pub struct Jobs {
|
||||||
pub futures: FuturesUnordered<JobFuture>,
|
/// jobs that need to complete before we exit.
|
||||||
/// These are the ones that need to complete before we exit.
|
|
||||||
pub wait_futures: FuturesUnordered<JobFuture>,
|
pub wait_futures: FuturesUnordered<JobFuture>,
|
||||||
|
pub callbacks: Receiver<Callback>,
|
||||||
|
pub status_messages: Receiver<StatusMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Job {
|
impl Job {
|
||||||
|
@ -52,8 +76,16 @@ impl Job {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Jobs {
|
impl Jobs {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
let (tx, rx) = channel(1024);
|
||||||
|
let _ = JOB_QUEUE.set(tx);
|
||||||
|
let status_messages = helix_event::status::setup();
|
||||||
|
Self {
|
||||||
|
wait_futures: FuturesUnordered::new(),
|
||||||
|
callbacks: rx,
|
||||||
|
status_messages,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
|
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
|
||||||
|
@ -85,18 +117,17 @@ impl Jobs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
|
|
||||||
tokio::select! {
|
|
||||||
event = self.futures.next() => { event }
|
|
||||||
event = self.wait_futures.next() => { event }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&self, j: Job) {
|
pub fn add(&self, j: Job) {
|
||||||
if j.wait {
|
if j.wait {
|
||||||
self.wait_futures.push(j.future);
|
self.wait_futures.push(j.future);
|
||||||
} else {
|
} else {
|
||||||
self.futures.push(j.future);
|
tokio::spawn(async move {
|
||||||
|
match j.future.await {
|
||||||
|
Ok(Some(cb)) => dispatch_callback(cb).await,
|
||||||
|
Ok(None) => (),
|
||||||
|
Err(err) => helix_event::status::report(err).await,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,17 @@ pub mod args;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod compositor;
|
pub mod compositor;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod events;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use futures_util::Future;
|
use futures_util::Future;
|
||||||
|
mod handlers;
|
||||||
|
|
||||||
use ignore::DirEntry;
|
use ignore::DirEntry;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
||||||
commands::{self, OnKeyCallback},
|
commands::{self, OnKeyCallback},
|
||||||
compositor::{Component, Context, Event, EventResult},
|
compositor::{Component, Context, Event, EventResult},
|
||||||
job::{self, Callback},
|
job::{self, Callback},
|
||||||
|
events::{OnModeSwitch, PostCommand},
|
||||||
key,
|
key,
|
||||||
keymap::{KeymapResult, Keymaps},
|
keymap::{KeymapResult, Keymaps},
|
||||||
ui::{
|
ui::{
|
||||||
|
@ -835,11 +836,18 @@ impl EditorView {
|
||||||
|
|
||||||
let mut execute_command = |command: &commands::MappableCommand| {
|
let mut execute_command = |command: &commands::MappableCommand| {
|
||||||
command.execute(cxt);
|
command.execute(cxt);
|
||||||
|
helix_event::dispatch(PostCommand { command, cx: cxt });
|
||||||
let current_mode = cxt.editor.mode();
|
let current_mode = cxt.editor.mode();
|
||||||
match (last_mode, current_mode) {
|
match (last_mode, current_mode) {
|
||||||
(Mode::Normal, Mode::Insert) => {
|
(Mode::Normal, Mode::Insert) => {
|
||||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||||
// and record the command that got us into this mode.
|
// and record the command that got us into this mode.
|
||||||
|
if current_mode != last_mode {
|
||||||
|
helix_event::dispatch(OnModeSwitch {
|
||||||
|
old_mode: last_mode,
|
||||||
|
new_mode: current_mode,
|
||||||
|
cx: cxt,
|
||||||
|
});
|
||||||
|
|
||||||
// how we entered insert mode is important, and we should track that so
|
// how we entered insert mode is important, and we should track that so
|
||||||
// we can repeat the side effect.
|
// we can repeat the side effect.
|
||||||
|
@ -1004,7 +1012,7 @@ impl EditorView {
|
||||||
}
|
}
|
||||||
|
|
||||||
let area = completion.area(size, editor);
|
let area = completion.area(size, editor);
|
||||||
editor.last_completion = None;
|
editor.last_completion = Some(CompleteAction::Triggered);
|
||||||
self.last_insert.1.push(InsertEvent::TriggerCompletion);
|
self.last_insert.1.push(InsertEvent::TriggerCompletion);
|
||||||
|
|
||||||
// TODO : propagate required size on resize to completion too
|
// TODO : propagate required size on resize to completion too
|
||||||
|
@ -1265,7 +1273,7 @@ impl Component for EditorView {
|
||||||
editor: context.editor,
|
editor: context.editor,
|
||||||
count: None,
|
count: None,
|
||||||
register: None,
|
register: None,
|
||||||
callback: None,
|
callback: Vec::new(),
|
||||||
on_next_key_callback: None,
|
on_next_key_callback: None,
|
||||||
jobs: context.jobs,
|
jobs: context.jobs,
|
||||||
};
|
};
|
||||||
|
@ -1375,7 +1383,7 @@ impl Component for EditorView {
|
||||||
}
|
}
|
||||||
|
|
||||||
// appease borrowck
|
// appease borrowck
|
||||||
let callback = cx.callback.take();
|
let callbacks = take(&mut cx.callback);
|
||||||
|
|
||||||
// if the command consumed the last view, skip the render.
|
// if the command consumed the last view, skip the render.
|
||||||
// on the next loop cycle the Application will then terminate.
|
// on the next loop cycle the Application will then terminate.
|
||||||
|
@ -1394,6 +1402,16 @@ impl Component for EditorView {
|
||||||
if mode != Mode::Insert {
|
if mode != Mode::Insert {
|
||||||
doc.append_changes_to_history(view);
|
doc.append_changes_to_history(view);
|
||||||
}
|
}
|
||||||
|
let callback = if callbacks.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let callback: crate::compositor::Callback = Box::new(move |compositor, cx| {
|
||||||
|
for callback in callbacks {
|
||||||
|
callback(compositor, cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Some(callback)
|
||||||
|
};
|
||||||
|
|
||||||
EventResult::Consumed(callback)
|
EventResult::Consumed(callback)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ use helix_core::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::editor::Config;
|
use crate::editor::Config;
|
||||||
|
use crate::events::{DocumentDidChange, SelectionDidChange};
|
||||||
use crate::{DocumentId, Editor, Theme, View, ViewId};
|
use crate::{DocumentId, Editor, Theme, View, ViewId};
|
||||||
|
|
||||||
/// 8kB of buffer space for encoding and decoding `Rope`s.
|
/// 8kB of buffer space for encoding and decoding `Rope`s.
|
||||||
|
@ -1096,6 +1097,10 @@ impl Document {
|
||||||
// TODO: use a transaction?
|
// TODO: use a transaction?
|
||||||
self.selections
|
self.selections
|
||||||
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
|
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
|
||||||
|
helix_event::dispatch(SelectionDidChange {
|
||||||
|
doc: self,
|
||||||
|
view: view_id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the origin selection of the text in a document, i.e. where
|
/// Find the origin selection of the text in a document, i.e. where
|
||||||
|
@ -1149,6 +1154,14 @@ impl Document {
|
||||||
let success = transaction.changes().apply(&mut self.text);
|
let success = transaction.changes().apply(&mut self.text);
|
||||||
|
|
||||||
if success {
|
if success {
|
||||||
|
if emit_lsp_notification {
|
||||||
|
helix_event::dispatch(DocumentDidChange {
|
||||||
|
doc: self,
|
||||||
|
view: view_id,
|
||||||
|
old_text: &old_doc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for selection in self.selections.values_mut() {
|
for selection in self.selections.values_mut() {
|
||||||
*selection = selection
|
*selection = selection
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -1164,6 +1177,10 @@ impl Document {
|
||||||
view_id,
|
view_id,
|
||||||
selection.clone().ensure_invariants(self.text.slice(..)),
|
selection.clone().ensure_invariants(self.text.slice(..)),
|
||||||
);
|
);
|
||||||
|
helix_event::dispatch(SelectionDidChange {
|
||||||
|
doc: self,
|
||||||
|
view: view_id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.modified_since_accessed = true;
|
self.modified_since_accessed = true;
|
||||||
|
@ -1276,6 +1293,7 @@ impl Document {
|
||||||
}
|
}
|
||||||
|
|
||||||
if emit_lsp_notification {
|
if emit_lsp_notification {
|
||||||
|
// TODO: move to hook
|
||||||
// emit lsp notification
|
// emit lsp notification
|
||||||
for language_server in self.language_servers() {
|
for language_server in self.language_servers() {
|
||||||
let notify = language_server.text_document_did_change(
|
let notify = language_server.text_document_did_change(
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
||||||
align_view,
|
align_view,
|
||||||
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
|
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
|
||||||
graphics::{CursorKind, Rect},
|
graphics::{CursorKind, Rect},
|
||||||
|
handlers::Handlers,
|
||||||
info::Info,
|
info::Info,
|
||||||
input::KeyEvent,
|
input::KeyEvent,
|
||||||
register::Registers,
|
register::Registers,
|
||||||
|
@ -960,6 +961,7 @@ pub struct Editor {
|
||||||
/// field is set and any old requests are automatically
|
/// field is set and any old requests are automatically
|
||||||
/// canceled as a result
|
/// canceled as a result
|
||||||
pub completion_request_handle: Option<oneshot::Sender<()>>,
|
pub completion_request_handle: Option<oneshot::Sender<()>>,
|
||||||
|
pub handlers: Handlers,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Motion = Box<dyn Fn(&mut Editor)>;
|
pub type Motion = Box<dyn Fn(&mut Editor)>;
|
||||||
|
|
9
helix-view/src/events.rs
Normal file
9
helix-view/src/events.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use helix_core::Rope;
|
||||||
|
use helix_event::events;
|
||||||
|
|
||||||
|
use crate::{Document, ViewId};
|
||||||
|
|
||||||
|
events! {
|
||||||
|
DocumentDidChange<'a> { doc: &'a mut Document, view: ViewId, old_text: &'a Rope }
|
||||||
|
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
|
||||||
|
}
|
12
helix-view/src/handlers.rs
Normal file
12
helix-view/src/handlers.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use helix_event::send_blocking;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
use crate::handlers::lsp::SignatureHelpInvoked;
|
||||||
|
use crate::Editor;
|
||||||
|
|
||||||
|
pub mod dap;
|
||||||
|
pub mod lsp;
|
||||||
|
|
||||||
|
pub struct Handlers {}
|
|
@ -1 +1,40 @@
|
||||||
|
use crate::{DocumentId, ViewId};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct CompletionTrigger {
|
||||||
|
/// The char position of the primary cursor when the
|
||||||
|
/// completion was triggered
|
||||||
|
pub trigger_pos: usize,
|
||||||
|
pub doc: DocumentId,
|
||||||
|
pub view: ViewId,
|
||||||
|
/// Whether the cause of the trigger was an automatic completion (any word
|
||||||
|
/// char for words longer than minimum word length).
|
||||||
|
/// This is false for trigger chars send by the LS
|
||||||
|
pub auto: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CompletionEvent {
|
||||||
|
/// Auto completion was triggered by typing a word char
|
||||||
|
/// or a completion trigger
|
||||||
|
Trigger(CompletionTrigger),
|
||||||
|
/// A completion was manually requested (c-x)
|
||||||
|
Manual,
|
||||||
|
/// Some text was deleted and the cursor is now at `pos`
|
||||||
|
DeleteText { pos: usize },
|
||||||
|
/// Invalidate the current auto completion trigger
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum SignatureHelpInvoked {
|
||||||
|
Automatic,
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SignatureHelpEvent {
|
||||||
|
Invoked,
|
||||||
|
Trigger,
|
||||||
|
ReTrigger,
|
||||||
|
Cancel,
|
||||||
|
RequestComplete { open: bool },
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
|
||||||
|
pub mod base64;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
|
pub mod events;
|
||||||
pub mod graphics;
|
pub mod graphics;
|
||||||
pub mod gutter;
|
pub mod gutter;
|
||||||
pub mod handlers {
|
pub mod handlers;
|
||||||
pub mod dap;
|
|
||||||
pub mod lsp;
|
|
||||||
}
|
|
||||||
pub mod base64;
|
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
|
|
Loading…
Add table
Reference in a new issue