diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 95faa01b0a3e..f9d508663c7a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,5 +1,5 @@ use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; +use futures_util::{future::OptionFuture, Stream}; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -11,7 +11,7 @@ use helix_view::{ document::DocumentSavedEventResult, editor::{ConfigEvent, EditorEvent}, graphics::Rect, - theme, + theme::{self, Modifier, Style}, tree::Layout, Align, Editor, }; @@ -23,6 +23,7 @@ use crate::{ commands::apply_workspace_edit, compositor::{Compositor, Event}, config::Config, + ctrl, job::Jobs, keymap::Keymaps, ui::{self, overlay::overlayed}, @@ -284,6 +285,13 @@ impl Application { // reset cursor cache self.editor.cursor_cache.set(None); + if self.jobs.blocking_job.is_some() { + surface.set_style( + area.clip_bottom(1), + Style::default().add_modifier(Modifier::DIM), + ); + } + let pos = pos.map(|pos| (pos.col as u16, pos.row as u16)); self.terminal.draw(pos, kind).unwrap(); } @@ -316,6 +324,13 @@ impl Application { tokio::select! { biased; + Some(callback) = OptionFuture::from(self.jobs.blocking_job.as_mut()), if self.jobs.blocking_job.is_some() => { + self.jobs.blocking_job = None; + self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); + self.editor.clear_status(); + self.render().await; + } + Some(signal) = self.signals.next() => { self.handle_signals(signal).await; } @@ -633,7 +648,24 @@ impl Application { kind: crossterm::event::KeyEventKind::Release, .. }) => false, - event => self.compositor.handle_event(&event.into(), &mut cx), + + CrosstermEvent::Key(event) + if cx.jobs.blocking_job.is_some() + && helix_view::input::KeyEvent::from(event) == ctrl!('c') => + { + cx.editor.clear_status(); + cx.jobs.blocking_job = None; + + true + } + + event => { + if cx.jobs.blocking_job.is_some() { + false + } else { + self.compositor.handle_event(&event.into(), &mut cx) + } + } }; if should_redraw && !self.editor.should_close() { @@ -874,7 +906,9 @@ impl Application { if !self.lsp_progress.is_progressing(server_id) { editor_view.spinners_mut().get_or_create(server_id).stop(); } - self.editor.clear_status(); + if self.config.load().editor.lsp.display_messages { + self.editor.clear_status(); + } // we want to render to clear any leftover spinners or messages return; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1c1edece1a31..c1284d090ec6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -51,7 +51,7 @@ use crate::{ args, compositor::{self, Component, Compositor}, filter_picker_entry, - job::Callback, + job::{cancelation, BlockingJob, Callback, CancelSender}, keymap::ReverseKeymap, ui::{ self, editor::InsertEvent, overlay::overlayed, FilePicker, Picker, Popup, Prompt, @@ -85,6 +85,7 @@ pub struct Context<'a> { pub editor: &'a mut Editor, pub callback: Option, + pub blocking_callback: Option, pub on_next_key_callback: Option, pub jobs: &'a mut Jobs, } @@ -97,6 +98,15 @@ impl<'a> Context<'a> { })); } + pub fn push_layer_blocking( + &mut self, + layer: impl Future>> + 'static, + cancel: CancelSender, + msg: &'static str, + ) { + self.blocking_callback = Some(BlockingJob::push_layer(layer, cancel, msg)) + } + #[inline] pub fn on_next_key( &mut self, @@ -117,6 +127,27 @@ impl<'a> Context<'a> { self.jobs.callback(make_job_callback(call, callback)); } + pub fn callback_blocking( + &mut self, + // The editor status message while awaiting the callback. + msg: &'static str, + call: impl Future> + 'static + Send, + callback: F, + ) where + T: for<'de> serde::Deserialize<'de> + Send + 'static, + F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static, + { + // No cancelation subscribers -- the only cleanup we need to do is to clear the + // status message, which is handled by BlockingJob's integration into the UI + // thread. + let (sender, _) = cancelation(); + self.blocking_callback = Some(BlockingJob::new( + make_job_callback(call, callback), + sender, + msg, + )) + } + /// Returns 1 if no explicit count was provided #[inline] pub fn count(&self) -> usize { @@ -2647,6 +2678,7 @@ pub fn command_palette(cx: &mut Context) { callback: None, on_next_key_callback: None, jobs: cx.jobs, + blocking_callback: None, }; let focus = view!(ctx.editor).id; diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 0b0d1db4d4bc..955163afc84e 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1007,22 +1007,19 @@ pub fn goto_definition(cx: &mut Context) { let pos = doc.position(view.id, offset_encoding); - let future = match language_server.goto_definition(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-definition"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, - ); + if let Some(future) = language_server.goto_definition(doc.identifier(), pos, None) { + cx.callback_blocking( + "goto_definition: Waiting on LSP...", + future, + move |editor, compositor, response: Option| { + let items = to_locations(response); + goto_impl(editor, compositor, items, offset_encoding); + }, + ); + } else { + cx.editor + .set_error("Language server does not support goto-definition"); + } } pub fn goto_type_definition(cx: &mut Context) { @@ -1082,22 +1079,19 @@ pub fn goto_reference(cx: &mut Context) { let pos = doc.position(view.id, offset_encoding); - let future = match language_server.goto_reference(doc.identifier(), pos, None) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support goto-reference"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option>| { - let items = response.unwrap_or_default(); - goto_impl(editor, compositor, items, offset_encoding); - }, - ); + if let Some(future) = language_server.goto_reference(doc.identifier(), pos, None) { + cx.callback_blocking( + "goto_reference: Waiting on LSP...", + future, + move |editor, compositor, response: Option>| { + let items = response.unwrap_or_default(); + goto_impl(editor, compositor, items, offset_encoding); + }, + ); + } else { + cx.editor + .set_error("Language server does not support goto-reference"); + } } #[derive(PartialEq, Eq)] diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index bcb3e44904e4..8a9ac6102685 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -1,6 +1,7 @@ // Each component declares its own size constraints and gets fitted based on its parent. // Q: how does this work with popups? // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), ) +use helix_core::diagnostic::Severity; use helix_core::Position; use helix_view::graphics::{CursorKind, Rect}; @@ -15,7 +16,7 @@ pub enum EventResult { Consumed(Option), } -use crate::job::Jobs; +use crate::job::{BlockingJob, Jobs}; use helix_view::Editor; pub use helix_view::input::Event; @@ -27,6 +28,10 @@ pub struct Context<'a> { } impl<'a> Context<'a> { + pub fn blocking_job(&mut self, blocking_callback: BlockingJob) { + self.editor.status_msg = Some((blocking_callback.msg.into(), Severity::Error)); + self.jobs.blocking_job = Some(blocking_callback); + } /// Waits on all pending jobs, and then tries to flush all pending write /// operations for all documents. pub fn block_try_flush_writes(&mut self) -> anyhow::Result<()> { diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 19f2521a5231..822f3fc928a8 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -1,46 +1,140 @@ +use crate::compositor::{Component, Compositor}; use helix_view::Editor; +use std::pin::Pin; +use std::task::Poll; +use tokio::sync::watch; -use crate::compositor::Compositor; - -use futures_util::future::{BoxFuture, Future, FutureExt}; +use futures_util::future::{Future, FutureExt}; use futures_util::stream::{FuturesUnordered, StreamExt}; -pub type EditorCompositorCallback = Box; -pub type EditorCallback = Box; +pub type EditorCompositorCallback = Box; +pub type EditorCallback = Box; pub enum Callback { EditorCompositor(EditorCompositorCallback), Editor(EditorCallback), } -pub type JobFuture = BoxFuture<'static, anyhow::Result>>; +pub type JobFuture = Pin>>>>; pub struct Job { - pub future: BoxFuture<'static, anyhow::Result>>, + pub future: JobFuture, /// Do we need to wait for this job to finish before exiting? pub wait: bool, } +pub fn cancelation() -> (CancelSender, CancelReciver) { + let (sender, reciver) = watch::channel(()); + (CancelSender(sender), CancelReciver(reciver)) +} + +#[derive(Debug)] +pub struct CancelSender(watch::Sender<()>); + +/// A cancel flag that can be awaited in an async context +/// and cheaply checked with a non-blocking check for use in +/// a synchronous call +// using a watch intead of a Notify so we can implement is_cancelled +#[derive(Clone, Debug)] +pub struct CancelReciver(watch::Receiver<()>); + +impl CancelReciver { + pub fn is_cancelled(&self) -> bool { + !matches!(self.0.has_changed(), Ok(false)) + } + + pub async fn canceled(mut self) { + let _ = self.0.changed().await; + } +} + +/// A Blocking Job is a job that would normally be executed synchronously +/// and block the UI thread but is performed asynchrounsly instead so that: +/// * The UI doesn't freeze (when resizing the window for example) +/// * We don't perform blocking tasks on a normal tokio thread +/// * The user can cancel an unresponsive task with C-c +pub struct BlockingJob { + future: JobFuture, + // When a BlockingJob is dropped all watchers are notified by the Drop + // implementation of watch::channel. + cancel: CancelSender, + pub msg: &'static str, +} + +impl BlockingJob { + pub fn new> + 'static>( + f: F, + cancel: CancelSender, + msg: &'static str, + ) -> BlockingJob { + BlockingJob { + future: Box::pin(f.map(|r| r.map(Some))), + cancel, + msg, + } + } + + pub fn push_layer>> + 'static>( + layer: F, + cancel: CancelSender, + msg: &'static str, + ) -> BlockingJob { + BlockingJob::new( + async { + let layer = layer.await?; + let callback = + Callback::EditorCompositor(Box::new(move |_, compositor: &mut Compositor| { + compositor.push(layer) + })); + Ok(callback) + }, + cancel, + msg, + ) + } + + pub fn non_blocking(self) -> Job { + Job { + future: Box::pin(self.future.map(move |res| { + drop(self.cancel); + res + })), + wait: false, + } + } + + pub fn cancel(&self) { + let _ = self.cancel.0.send(()); + } +} + +impl Future for BlockingJob { + type Output = anyhow::Result>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + self.future.poll_unpin(cx) + } +} + #[derive(Default)] pub struct Jobs { pub futures: FuturesUnordered, /// These are the ones that need to complete before we exit. pub wait_futures: FuturesUnordered, + pub blocking_job: Option, } impl Job { - pub fn new> + Send + 'static>(f: F) -> Self { + pub fn new> + 'static>(f: F) -> Self { Self { - future: f.map(|r| r.map(|()| None)).boxed(), + future: Box::pin(f.map(|r| r.map(|()| None))), wait: false, } } - pub fn with_callback> + Send + 'static>( - f: F, - ) -> Self { + pub fn with_callback> + 'static>(f: F) -> Self { Self { - future: f.map(|r| r.map(Some)).boxed(), + future: Box::pin(f.map(|r| r.map(Some))), wait: false, } } @@ -56,14 +150,11 @@ impl Jobs { Self::default() } - pub fn spawn> + Send + 'static>(&mut self, f: F) { + pub fn spawn> + 'static>(&mut self, f: F) { self.add(Job::new(f)); } - pub fn callback> + Send + 'static>( - &mut self, - f: F, - ) { + pub fn callback> + 'static>(&mut self, f: F) { self.add(Job::with_callback(f)); } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7c22df747642..8844555af7b7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1212,6 +1212,7 @@ impl Component for EditorView { callback: None, on_next_key_callback: None, jobs: context.jobs, + blocking_callback: None, }; match event { @@ -1307,7 +1308,15 @@ impl Component for EditorView { } // appease borrowck - let callback = cx.callback.take(); + let mut callback = cx.callback.take(); + if let Some(blocking_callback) = cx.blocking_callback.take() { + callback = Some(Box::new(move |compositor, cx| { + cx.blocking_job(blocking_callback); + if let Some(callback) = callback { + callback(compositor, cx) + } + })); + } // if the command consumed the last view, skip the render. // on the next loop cycle the Application will then terminate.