Show status of LSP actions (#9818)

Daniel Zhu and Mikayla created

Fixes #4380

Parts im still unsure about:
- [x] where exactly I should call `on_lsp_start`/`on_lsp_end`
- [x] how to handle things better than `let is_references =
TypeId::of::<R>() == TypeId::of::<GetReferences>();`, which feels very
janky
- [x] I want to have the message be something like `"Finding references
to [...]"` instead of just `textDocument/references`, but I'm not sure
how to retrieve the name of the symbol that's being queried
- [ ] I think the bulk of the runtime is occupied by `let result =
language_server.request::<R::LspRequest>(lsp_params).await;`, but since
`ModelContext` isn't passed into it, I'm not sure how to update progress
from within that function
- [x] A good way to disambiguate between multiple calls to the same lsp
function; im currently using the function name itself as the unique
identifier for that request, which could create issues if multiple
`textDocument/references` requests are sent in parallel

Any help with these would be deeply appreciated!

Release Notes:

- Adds a status indicator for LSP actions

---------

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/lsp/src/lsp.rs             | 44 ++++++++++++++++++--
crates/project/src/lsp_command.rs |  8 +++
crates/project/src/project.rs     | 69 +++++++++++++++++++++++++-------
3 files changed, 99 insertions(+), 22 deletions(-)

Detailed changes

crates/lsp/src/lsp.rs 🔗

@@ -4,7 +4,7 @@ pub use lsp_types::*;
 
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
-use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, FutureExt};
+use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt};
 use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
 use parking_lot::Mutex;
 use postage::{barrier, prelude::Stream};
@@ -22,14 +22,15 @@ use smol::process::windows::CommandExt;
 use std::{
     ffi::OsString,
     fmt,
-    future::Future,
     io::Write,
     path::PathBuf,
+    pin::Pin,
     str::{self, FromStr as _},
     sync::{
         atomic::{AtomicI32, Ordering::SeqCst},
         Arc, Weak,
     },
+    task::Poll,
     time::{Duration, Instant},
 };
 use std::{path::Path, process::Stdio};
@@ -168,6 +169,37 @@ struct Error {
     message: String,
 }
 
+pub trait LspRequestFuture<O>: Future<Output = O> {
+    fn id(&self) -> i32;
+}
+
+struct LspRequest<F> {
+    id: i32,
+    request: F,
+}
+
+impl<F> LspRequest<F> {
+    pub fn new(id: i32, request: F) -> Self {
+        Self { id, request }
+    }
+}
+
+impl<F: Future> Future for LspRequest<F> {
+    type Output = F::Output;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
+        // SAFETY: This is standard pin projection, we're pinned so our fields must be pinned.
+        let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().request) };
+        inner.poll(cx)
+    }
+}
+
+impl<F: Future> LspRequestFuture<F::Output> for LspRequest<F> {
+    fn id(&self) -> i32 {
+        self.id
+    }
+}
+
 /// Experimental: Informs the end user about the state of the server
 ///
 /// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status)
@@ -916,7 +948,7 @@ impl LanguageServer {
     pub fn request<T: request::Request>(
         &self,
         params: T::Params,
-    ) -> impl Future<Output = Result<T::Result>>
+    ) -> impl LspRequestFuture<Result<T::Result>>
     where
         T::Result: 'static + Send,
     {
@@ -935,7 +967,7 @@ impl LanguageServer {
         outbound_tx: &channel::Sender<String>,
         executor: &BackgroundExecutor,
         params: T::Params,
-    ) -> impl 'static + Future<Output = anyhow::Result<T::Result>>
+    ) -> impl LspRequestFuture<Result<T::Result>>
     where
         T::Result: 'static + Send,
     {
@@ -984,7 +1016,7 @@ impl LanguageServer {
         let outbound_tx = outbound_tx.downgrade();
         let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse();
         let started = Instant::now();
-        async move {
+        LspRequest::new(id, async move {
             handle_response?;
             send?;
 
@@ -1014,7 +1046,7 @@ impl LanguageServer {
                     anyhow::bail!("LSP request timeout");
                 }
             }
-        }
+        })
     }
 
     /// Sends a RPC notification to the language server.

crates/project/src/lsp_command.rs 🔗

@@ -41,6 +41,10 @@ pub trait LspCommand: 'static + Sized + Send {
         true
     }
 
+    fn status(&self) -> Option<String> {
+        None
+    }
+
     fn to_lsp(
         &self,
         path: &Path,
@@ -895,6 +899,10 @@ impl LspCommand for GetReferences {
     type LspRequest = lsp::request::References;
     type ProtoRequest = proto::GetReferences;
 
+    fn status(&self) -> Option<String> {
+        return Some("Finding references...".to_owned());
+    }
+
     fn to_lsp(
         &self,
         path: &Path,

crates/project/src/project.rs 🔗

@@ -55,7 +55,8 @@ use log::error;
 use lsp::{
     DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
     DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId,
-    MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus, ServerStatus,
+    LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus,
+    ServerStatus,
 };
 use lsp_command::*;
 use node_runtime::NodeRuntime;
@@ -5697,7 +5698,7 @@ impl Project {
     }
 
     fn code_actions_impl(
-        &self,
+        &mut self,
         buffer_handle: &Model<Buffer>,
         range: Range<Anchor>,
         cx: &mut ModelContext<Self>,
@@ -5777,7 +5778,7 @@ impl Project {
     }
 
     pub fn code_actions<T: Clone + ToOffset>(
-        &self,
+        &mut self,
         buffer_handle: &Model<Buffer>,
         range: Range<T>,
         cx: &mut ModelContext<Self>,
@@ -6131,7 +6132,7 @@ impl Project {
     }
 
     fn prepare_rename_impl(
-        &self,
+        &mut self,
         buffer: Model<Buffer>,
         position: PointUtf16,
         cx: &mut ModelContext<Self>,
@@ -6144,7 +6145,7 @@ impl Project {
         )
     }
     pub fn prepare_rename<T: ToPointUtf16>(
-        &self,
+        &mut self,
         buffer: Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
@@ -6154,7 +6155,7 @@ impl Project {
     }
 
     fn perform_rename_impl(
-        &self,
+        &mut self,
         buffer: Model<Buffer>,
         position: PointUtf16,
         new_name: String,
@@ -6174,7 +6175,7 @@ impl Project {
         )
     }
     pub fn perform_rename<T: ToPointUtf16>(
-        &self,
+        &mut self,
         buffer: Model<Buffer>,
         position: T,
         new_name: String,
@@ -6186,7 +6187,7 @@ impl Project {
     }
 
     pub fn on_type_format_impl(
-        &self,
+        &mut self,
         buffer: Model<Buffer>,
         position: PointUtf16,
         trigger: String,
@@ -6210,7 +6211,7 @@ impl Project {
     }
 
     pub fn on_type_format<T: ToPointUtf16>(
-        &self,
+        &mut self,
         buffer: Model<Buffer>,
         position: T,
         trigger: String,
@@ -6222,7 +6223,7 @@ impl Project {
     }
 
     pub fn inlay_hints<T: ToOffset>(
-        &self,
+        &mut self,
         buffer_handle: Model<Buffer>,
         range: Range<T>,
         cx: &mut ModelContext<Self>,
@@ -6232,7 +6233,7 @@ impl Project {
         self.inlay_hints_impl(buffer_handle, range, cx)
     }
     fn inlay_hints_impl(
-        &self,
+        &mut self,
         buffer_handle: Model<Buffer>,
         range: Range<Anchor>,
         cx: &mut ModelContext<Self>,
@@ -6711,12 +6712,34 @@ impl Project {
             let file = File::from_dyn(buffer.file()).and_then(File::as_local);
             if let (Some(file), Some(language_server)) = (file, language_server) {
                 let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
+                let status = request.status();
                 return cx.spawn(move |this, cx| async move {
                     if !request.check_capabilities(language_server.capabilities()) {
                         return Ok(Default::default());
                     }
 
-                    let result = language_server.request::<R::LspRequest>(lsp_params).await;
+                    let lsp_request = language_server.request::<R::LspRequest>(lsp_params);
+
+                    let id = lsp_request.id();
+                    if status.is_some() {
+                        cx.update(|cx| {
+                            this.update(cx, |this, cx| {
+                                this.on_lsp_work_start(
+                                    language_server.server_id(),
+                                    id.to_string(),
+                                    LanguageServerProgress {
+                                        message: status.clone(),
+                                        percentage: None,
+                                        last_update_at: Instant::now(),
+                                    },
+                                    cx,
+                                );
+                            })
+                        })
+                        .log_err();
+                    }
+
+                    let result = lsp_request.await;
                     let response = match result {
                         Ok(response) => response,
 
@@ -6729,16 +6752,30 @@ impl Project {
                             return Err(err);
                         }
                     };
-
-                    request
+                    let result = request
                         .response_from_lsp(
                             response,
                             this.upgrade().ok_or_else(|| anyhow!("no app context"))?,
                             buffer_handle,
                             language_server.server_id(),
-                            cx,
+                            cx.clone(),
                         )
-                        .await
+                        .await;
+
+                    if status.is_some() {
+                        cx.update(|cx| {
+                            this.update(cx, |this, cx| {
+                                this.on_lsp_work_end(
+                                    language_server.server_id(),
+                                    id.to_string(),
+                                    cx,
+                                );
+                            })
+                        })
+                        .log_err();
+                    }
+
+                    result
                 });
             }
         } else if let Some(project_id) = self.remote_id() {