aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPascal Kuthe2023-11-30 23:03:26 +0000
committerBlaž Hrastnik2024-01-23 02:20:19 +0000
commit13ed4f6c4748019787d24c2b686d417b71604242 (patch)
tree8263b77ee05a22dfc85410345069efb3668b1877
parent7d7ace551cd58f0b6d65af7a6dfa8f896d94724a (diff)
Add hook/event system
-rw-r--r--.cargo/config.toml14
-rw-r--r--Cargo.lock6
-rw-r--r--helix-event/Cargo.toml17
-rw-r--r--helix-event/src/cancel.rs19
-rw-r--r--helix-event/src/debounce.rs67
-rw-r--r--helix-event/src/hook.rs91
-rw-r--r--helix-event/src/lib.rs201
-rw-r--r--helix-event/src/redraw.rs24
-rw-r--r--helix-event/src/registry.rs131
-rw-r--r--helix-event/src/runtime.rs88
-rw-r--r--helix-event/src/status.rs68
-rw-r--r--helix-event/src/test.rs90
-rw-r--r--helix-term/Cargo.toml2
-rw-r--r--helix-term/src/application.rs24
-rw-r--r--helix-term/src/commands.rs26
-rw-r--r--helix-term/src/events.rs20
-rw-r--r--helix-term/src/handlers.rs15
-rw-r--r--helix-term/src/job.rs55
-rw-r--r--helix-term/src/lib.rs4
-rw-r--r--helix-term/src/ui/editor.rs24
-rw-r--r--helix-view/src/document.rs18
-rw-r--r--helix-view/src/editor.rs2
-rw-r--r--helix-view/src/events.rs9
-rw-r--r--helix-view/src/handlers.rs12
-rw-r--r--helix-view/src/handlers/lsp.rs39
-rw-r--r--helix-view/src/lib.rs8
26 files changed, 1024 insertions, 50 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index b016eca3..af4312dc 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -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]
xtask = "run --package xtask --"
integration-test = "test --features integration --profile integration --workspace --test integration"
+
diff --git a/Cargo.lock b/Cargo.lock
index 9884dadf..4969ef46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1102,6 +1102,12 @@ dependencies = [
name = "helix-event"
version = "23.10.0"
dependencies = [
+ "ahash",
+ "anyhow",
+ "futures-executor",
+ "hashbrown 0.14.3",
+ "log",
+ "once_cell",
"parking_lot",
"tokio",
]
diff --git a/helix-event/Cargo.toml b/helix-event/Cargo.toml
index c2032824..a5c88e93 100644
--- a/helix-event/Cargo.toml
+++ b/helix-event/Cargo.toml
@@ -12,5 +12,18 @@ homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
-parking_lot = { version = "0.12", features = ["send_guard"] }
+ahash = "0.8.3"
+hashbrown = "0.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 = []
diff --git a/helix-event/src/cancel.rs b/helix-event/src/cancel.rs
new file mode 100644
index 00000000..f027be80
--- /dev/null
+++ b/helix-event/src/cancel.rs
@@ -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)
+ }
+ }
+}
diff --git a/helix-event/src/debounce.rs b/helix-event/src/debounce.rs
new file mode 100644
index 00000000..30b6f671
--- /dev/null
+++ b/helix-event/src/debounce.rs
@@ -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)));
+ }
+}
diff --git a/helix-event/src/hook.rs b/helix-event/src/hook.rs
new file mode 100644
index 00000000..7fb68148
--- /dev/null
+++ b/helix-event/src/hook.rs
@@ -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 {}
diff --git a/helix-event/src/lib.rs b/helix-event/src/lib.rs
index 9c082b93..894de5e8 100644
--- a/helix-event/src/lib.rs
+++ b/helix-event/src/lib.rs
@@ -1,8 +1,203 @@
//! `helix-event` contains systems that allow (often async) communication between
-//! different editor components without strongly coupling them. Currently this
-//! crate only contains some smaller facilities but the intend is to add more
-//! functionality in the future ( like a generic hook system)
+//! different editor components without strongly coupling them. Specifically
+//! it allows defining synchronous hooks that run when certain editor events
+//! occur.
+//!
+//! The core of the event system are hook callbacks and the [`Event`] trait. A
+//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>`
+//! that gets called every time an appropriate event is dispatched. The implementation
+//! details of the [`Event`] trait are considered private. The [`events`] macro is
+//! provided which automatically declares event types. Similarly the `register_hook`
+//! macro should be used to (safely) declare event hooks.
+//!
+//! Hooks run synchronously which can be advantageous since they can modify the
+//! current editor state right away (for example to immediately hide the completion
+//! popup). However, they can not contain their own state without locking since
+//! they only receive immutable references. For handler that want to track state, do
+//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
+//! Async hooks are based around a channels that receive events specific to
+//! that `AsyncHook` (usually an enum). These events can be sent by synchronous
+//! hooks. Due to some limitations around tokio channels the [`send_blocking`]
+//! function exported in this crate should be used instead of the builtin
+//! `blocking_send`.
+//!
+//! In addition to the core event system, this crate contains some message queues
+//! that allow transfer of data back to the main event loop from async hooks and
+//! hooks that may not have access to all application data (for example in helix-view).
+//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and
+//! display status messages ([`status`]).
+//!
+//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the
+//! main loop (including access to the compositor). Ideally that queue will be moved
+//! to helix-view in the future if we manage to detach the compositor from its rendering backend.
+use anyhow::Result;
+pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
+pub use debounce::{send_blocking, AsyncHook};
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
+pub use registry::Event;
+mod cancel;
+mod debounce;
+mod hook;
mod redraw;
+mod registry;
+#[doc(hidden)]
+pub mod runtime;
+pub mod status;
+
+#[cfg(test)]
+mod test;
+
+pub fn register_event<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);
+ }
+ };
+}
diff --git a/helix-event/src/redraw.rs b/helix-event/src/redraw.rs
index a9915223..8fadb8ae 100644
--- a/helix-event/src/redraw.rs
+++ b/helix-event/src/redraw.rs
@@ -5,16 +5,20 @@ use std::future::Future;
use parking_lot::{RwLock, RwLockReadGuard};
use tokio::sync::Notify;
-/// A `Notify` instance that can be used to (asynchronously) request
-/// the editor the render a new frame.
-static REDRAW_NOTIFY: Notify = Notify::const_new();
-
-/// A `RwLock` that prevents the next frame from being
-/// drawn until an exclusive (write) lock can be acquired.
-/// This allows asynchsonous 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(());
+use crate::runtime_local;
+
+runtime_local! {
+ /// A `Notify` instance that can be used to (asynchronously) request
+ /// the editor to render a new frame.
+ static REDRAW_NOTIFY: Notify = Notify::const_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, ()>;
diff --git a/helix-event/src/registry.rs b/helix-event/src/registry.rs
new file mode 100644
index 00000000..d43c48ac
--- /dev/null
+++ b/helix-event/src/registry.rs
@@ -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(&REGISTRY.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;
+}
diff --git a/helix-event/src/runtime.rs b/helix-event/src/runtime.rs
new file mode 100644
index 00000000..8da465ef
--- /dev/null
+++ b/helix-event/src/runtime.rs
@@ -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);)*
+ };
+}
diff --git a/helix-event/src/status.rs b/helix-event/src/status.rs
new file mode 100644
index 00000000..fdca6762
--- /dev/null
+++ b/helix-event/src/status.rs
@@ -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
+}
diff --git a/helix-event/src/test.rs b/helix-event/src/test.rs
new file mode 100644
index 00000000..a1283ada
--- /dev/null
+++ b/helix-event/src/test.rs
@@ -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)
+}
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 21c35553..7bdd433e 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -15,7 +15,7 @@ homepage.workspace = true
[features]
default = ["git"]
unicode-lines = ["helix-core/unicode-lines"]
-integration = []
+integration = ["helix-event/integration_test"]
git = ["helix-vcs/git"]
[[bin]]
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 290441b4..8215eeaa 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,6 +1,10 @@
use arc_swap::{access::Map, ArcSwap};
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::{
lsp::{self, notification::Notification},
util::lsp_range_to_range,
@@ -24,6 +28,7 @@ use crate::{
commands::apply_workspace_edit,
compositor::{Compositor, Event},
config::Config,
+ handlers,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlaid},
@@ -138,6 +143,7 @@ impl Application {
let area = terminal.size().expect("couldn't get terminal size");
let mut compositor = Compositor::new(area);
let config = Arc::new(ArcSwap::from_pointee(config));
+ let handlers = handlers::setup(config.clone());
let mut editor = Editor::new(
area,
theme_loader.clone(),
@@ -145,6 +151,7 @@ impl Application {
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor
})),
+ handlers,
);
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@@ -321,10 +328,21 @@ impl Application {
Some(event) = input_stream.next() => {
self.handle_terminal_events(event).await;
}
- Some(callback) = self.jobs.futures.next() => {
- self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
+ Some(callback) = self.jobs.callbacks.recv() => {
+ self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
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() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render().await;
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 53783e4e..48ceb23b 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -88,7 +88,7 @@ pub struct Context<'a> {
pub count: Option<NonZeroUsize>,
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 jobs: &'a mut Jobs,
}
@@ -96,16 +96,18 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) {
- self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
- compositor.push(component)
- }));
+ self.callback
+ .push(Box::new(|compositor: &mut Compositor, _| {
+ compositor.push(component)
+ }));
}
/// Call `replace_or_push` on the Compositor
pub fn replace_or_push_layer<T: Component>(&mut self, id: &'static str, component: T) {
- self.callback = Some(Box::new(move |compositor: &mut Compositor, _| {
- compositor.replace_or_push(id, component);
- }));
+ self.callback
+ .push(Box::new(move |compositor: &mut Compositor, _| {
+ compositor.replace_or_push(id, component);
+ }));
}
#[inline]
@@ -2934,7 +2936,7 @@ pub fn command_palette(cx: &mut Context) {
let register = cx.register;
let count = cx.count;
- cx.callback = Some(Box::new(
+ cx.callback.push(Box::new(
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
[&cx.editor.mode]
@@ -2954,7 +2956,7 @@ pub fn command_palette(cx: &mut Context) {
register,
count,
editor: cx.editor,
- callback: None,
+ callback: Vec::new(),
on_next_key_callback: None,
jobs: cx.jobs,
};
@@ -2982,7 +2984,7 @@ pub fn command_palette(cx: &mut Context) {
fn last_picker(cx: &mut Context) {
// 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() {
compositor.push(picker);
} else {
@@ -3494,6 +3496,7 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
}
pub mod insert {
+ use crate::events::PostInsertChar;
use super::*;
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
pub type PostHook = fn(&mut Context, char);
@@ -3627,6 +3630,7 @@ pub mod insert {
for hook in &[language_server_completion, signature_help] {
hook(cx, c);
}
+ helix_event::dispatch(PostInsertChar { c, cx });
}
pub fn smart_tab(cx: &mut Context) {
@@ -5820,7 +5824,7 @@ fn replay_macro(cx: &mut Context) {
cx.editor.macro_replaying.push(reg);
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 &key in keys.iter() {
compositor.handle_event(&compositor::Event::Key(key), cx);
diff --git a/helix-term/src/events.rs b/helix-term/src/events.rs
new file mode 100644
index 00000000..49b44f77
--- /dev/null
+++ b/helix-term/src/events.rs
@@ -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>();
+}
diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs
new file mode 100644
index 00000000..ab2d724f
--- /dev/null
+++ b/helix-term/src/handlers.rs
@@ -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
+}
diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs
index 19f2521a..72ed892d 100644
--- a/helix-term/src/job.rs
+++ b/helix-term/src/job.rs
@@ -1,13 +1,37 @@
+use helix_event::status::StatusMessage;
+use helix_event::{runtime_local, send_blocking};
use helix_view::Editor;
+use once_cell::sync::OnceCell;
use crate::compositor::Compositor;
use futures_util::future::{BoxFuture, Future, FutureExt};
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 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 {
EditorCompositor(EditorCompositorCallback),
Editor(EditorCallback),
@@ -21,11 +45,11 @@ pub struct Job {
pub wait: bool,
}
-#[derive(Default)]
pub struct Jobs {
- pub futures: FuturesUnordered<JobFuture>,
- /// These are the ones that need to complete before we exit.
+ /// jobs that need to complete before we exit.
pub wait_futures: FuturesUnordered<JobFuture>,
+ pub callbacks: Receiver<Callback>,
+ pub status_messages: Receiver<StatusMessage>,
}
impl Job {
@@ -52,8 +76,16 @@ impl Job {
}
impl Jobs {
+ #[allow(clippy::new_without_default)]
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) {
@@ -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) {
if j.wait {
self.wait_futures.push(j.future);
} 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,
+ }
+ });
}
}
diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs
index a1d60329..b1413ed0 100644
--- a/helix-term/src/lib.rs
+++ b/helix-term/src/lib.rs
@@ -6,13 +6,17 @@ pub mod args;
pub mod commands;
pub mod compositor;
pub mod config;
+pub mod events;
pub mod health;
pub mod job;
pub mod keymap;
pub mod ui;
+
use std::path::Path;
use futures_util::Future;
+mod handlers;
+
use ignore::DirEntry;
use url::Url;
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 24fcdb01..9f186d14 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -2,6 +2,7 @@ use crate::{
commands::{self, OnKeyCallback},
compositor::{Component, Context, Event, EventResult},
job::{self, Callback},
+ events::{OnModeSwitch, PostCommand},
key,
keymap::{KeymapResult, Keymaps},
ui::{
@@ -835,11 +836,18 @@ impl EditorView {
let mut execute_command = |command: &commands::MappableCommand| {
command.execute(cxt);
+ helix_event::dispatch(PostCommand { command, cx: cxt });
let current_mode = cxt.editor.mode();
match (last_mode, current_mode) {
(Mode::Normal, Mode::Insert) => {
// HAXX: if we just entered insert mode from normal, clear key buf
// 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
// we can repeat the side effect.
@@ -1004,7 +1012,7 @@ impl EditorView {
}
let area = completion.area(size, editor);
- editor.last_completion = None;
+ editor.last_completion = Some(CompleteAction::Triggered);
self.last_insert.1.push(InsertEvent::TriggerCompletion);
// TODO : propagate required size on resize to completion too
@@ -1265,7 +1273,7 @@ impl Component for EditorView {
editor: context.editor,
count: None,
register: None,
- callback: None,
+ callback: Vec::new(),
on_next_key_callback: None,
jobs: context.jobs,
};
@@ -1375,7 +1383,7 @@ impl Component for EditorView {
}
// appease borrowck
- let callback = cx.callback.take();
+ let callbacks = take(&mut cx.callback);
// if the command consumed the last view, skip the render.
// on the next loop cycle the Application will then terminate.
@@ -1394,6 +1402,16 @@ impl Component for EditorView {
if mode != Mode::Insert {
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)
}
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 6473c2d1..93b83da4 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -36,6 +36,7 @@ use helix_core::{
};
use crate::editor::Config;
+use crate::events::{DocumentDidChange, SelectionDidChange};
use crate::{DocumentId, Editor, Theme, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -1096,6 +1097,10 @@ impl Document {
// TODO: use a transaction?
self.selections
.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
@@ -1149,6 +1154,14 @@ impl Document {
let success = transaction.changes().apply(&mut self.text);
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() {
*selection = selection
.clone()
@@ -1164,6 +1177,10 @@ impl Document {
view_id,
selection.clone().ensure_invariants(self.text.slice(..)),
);
+ helix_event::dispatch(SelectionDidChange {
+ doc: self,
+ view: view_id,
+ });
}
self.modified_since_accessed = true;
@@ -1276,6 +1293,7 @@ impl Document {
}
if emit_lsp_notification {
+ // TODO: move to hook
// emit lsp notification
for language_server in self.language_servers() {
let notify = language_server.text_document_did_change(
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 0ab4be8b..44c706d7 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -2,6 +2,7 @@ use crate::{
align_view,
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
graphics::{CursorKind, Rect},
+ handlers::Handlers,
info::Info,
input::KeyEvent,
register::Registers,
@@ -960,6 +961,7 @@ pub struct Editor {
/// field is set and any old requests are automatically
/// canceled as a result
pub completion_request_handle: Option<oneshot::Sender<()>>,
+ pub handlers: Handlers,
}
pub type Motion = Box<dyn Fn(&mut Editor)>;
diff --git a/helix-view/src/events.rs b/helix-view/src/events.rs
new file mode 100644
index 00000000..8b789cc0
--- /dev/null
+++ b/helix-view/src/events.rs
@@ -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 }
+}
diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs
new file mode 100644
index 00000000..ae3eb545
--- /dev/null
+++ b/helix-view/src/handlers.rs
@@ -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 {}
diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs
index 8b137891..95838564 100644
--- a/helix-view/src/handlers/lsp.rs
+++ b/helix-view/src/handlers/lsp.rs
@@ -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 },
+}
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index 6a68e7d6..82827b5d 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -1,17 +1,15 @@
#[macro_use]
pub mod macros;
+pub mod base64;
pub mod clipboard;
pub mod document;
pub mod editor;
pub mod env;
+pub mod events;
pub mod graphics;
pub mod gutter;
-pub mod handlers {
- pub mod dap;
- pub mod lsp;
-}
-pub mod base64;
+pub mod handlers;
pub mod info;
pub mod input;
pub mod keyboard;