Stop waiting for part of LSP responses on remote Collab clients' part (#36557)

Kirill Bulatov , David Kleingeld , Mikayla Maki , and David Kleingeld created

Instead of holding a connection for potentially long LSP queries (e.g.
rust-analyzer might take minutes to look up a definition), disconnect
right after sending the initial request and handle the follow-up
responses later.

As a bonus, this allows to cancel previously sent request on the local
Collab clients' side due to this, as instead of holding and serving the
old connection, local clients now can stop previous requests, if needed.

Current PR does not convert all LSP requests to the new paradigm, but
the problematic ones, deprecating `MultiLspQuery` and moving all its
requests to the new paradigm.

Release Notes:

- Improved resource usage when querying LSP over Collab

---------

Co-authored-by: David Kleingeld <git@davidsk.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: David Kleingeld <davidsk@zed.dev>

Change summary

crates/agent_ui/src/acp/message_editor.rs    |   8 
crates/collab/src/rpc.rs                     |  20 
crates/collab/src/tests/editor_tests.rs      | 208 ++++++
crates/collab/src/tests/integration_tests.rs |  12 
crates/editor/src/editor.rs                  |  24 
crates/editor/src/hover_links.rs             |   2 
crates/editor/src/hover_popover.rs           |   2 
crates/editor/src/proposed_changes_editor.rs |   4 
crates/editor/src/signature_help.rs          |   4 
crates/lsp/src/lsp.rs                        |   2 
crates/project/src/lsp_command.rs            |   3 
crates/project/src/lsp_store.rs              | 724 +++++++++------------
crates/project/src/project.rs                |  49 
crates/project/src/project_tests.rs          |   8 
crates/proto/proto/lsp.proto                 |  91 +-
crates/proto/proto/zed.proto                 |   5 
crates/proto/src/macros.rs                   |  29 
crates/proto/src/proto.rs                    |  45 +
crates/proto/src/typed_envelope.rs           |  52 +
crates/rpc/src/proto_client.rs               | 304 +++++++-
20 files changed, 1,029 insertions(+), 567 deletions(-)

Detailed changes

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -1691,7 +1691,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
         buffer: &Entity<Buffer>,
         position: text::Anchor,
         cx: &mut App,
-    ) -> Option<Task<Vec<project::Hover>>> {
+    ) -> Option<Task<Option<Vec<project::Hover>>>> {
         let snapshot = buffer.read(cx).snapshot();
         let offset = position.to_offset(&snapshot);
         let (start, end) = self.range.get()?;
@@ -1699,14 +1699,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
             return None;
         }
         let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
-        Some(Task::ready(vec![project::Hover {
+        Some(Task::ready(Some(vec![project::Hover {
             contents: vec![project::HoverBlock {
                 text: "Slash commands are not supported".into(),
                 kind: project::HoverBlockKind::PlainText,
             }],
             range: Some(range),
             language: None,
-        }]))
+        }])))
     }
 
     fn inline_values(
@@ -1756,7 +1756,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
         _position: text::Anchor,
         _kind: editor::GotoDefinitionKind,
         _cx: &mut App,
-    ) -> Option<Task<Result<Vec<project::LocationLink>>>> {
+    ) -> Option<Task<Result<Option<Vec<project::LocationLink>>>>> {
         None
     }
 

crates/collab/src/rpc.rs 🔗

@@ -400,6 +400,8 @@ impl Server {
             .add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
             .add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
             .add_request_handler(multi_lsp_query)
+            .add_request_handler(lsp_query)
+            .add_message_handler(broadcast_project_message_from_host::<proto::LspQueryResponse>)
             .add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
             .add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
             .add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
@@ -910,7 +912,9 @@ impl Server {
                                 user_id=field::Empty,
                                 login=field::Empty,
                                 impersonator=field::Empty,
+                                // todo(lsp) remove after Zed Stable hits v0.204.x
                                 multi_lsp_query_request=field::Empty,
+                                lsp_query_request=field::Empty,
                                 release_channel=field::Empty,
                                 { TOTAL_DURATION_MS }=field::Empty,
                                 { PROCESSING_DURATION_MS }=field::Empty,
@@ -2356,6 +2360,7 @@ where
     Ok(())
 }
 
+// todo(lsp) remove after Zed Stable hits v0.204.x
 async fn multi_lsp_query(
     request: MultiLspQuery,
     response: Response<MultiLspQuery>,
@@ -2366,6 +2371,21 @@ async fn multi_lsp_query(
     forward_mutating_project_request(request, response, session).await
 }
 
+async fn lsp_query(
+    request: proto::LspQuery,
+    response: Response<proto::LspQuery>,
+    session: MessageContext,
+) -> Result<()> {
+    let (name, should_write) = request.query_name_and_write_permissions();
+    tracing::Span::current().record("lsp_query_request", name);
+    tracing::info!("lsp_query message received");
+    if should_write {
+        forward_mutating_project_request(request, response, session).await
+    } else {
+        forward_read_only_project_request(request, response, session).await
+    }
+}
+
 /// Notify other participants that a new buffer has been created
 async fn create_buffer_for_peer(
     request: proto::CreateBufferForPeer,

crates/collab/src/tests/editor_tests.rs 🔗

@@ -15,13 +15,14 @@ use editor::{
     },
 };
 use fs::Fs;
-use futures::{StreamExt, lock::Mutex};
+use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
 use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
 use indoc::indoc;
 use language::{
     FakeLspAdapter,
     language_settings::{AllLanguageSettings, InlayHintSettings},
 };
+use lsp::LSP_REQUEST_TIMEOUT;
 use project::{
     ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
     lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
@@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
     })
 }
 
+#[gpui::test]
+async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    let mut server = TestServer::start(cx_a.executor()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    cx_b.update(editor::init);
+
+    let command_name = "test_command";
+    let capabilities = lsp::ServerCapabilities {
+        code_lens_provider: Some(lsp::CodeLensOptions {
+            resolve_provider: None,
+        }),
+        execute_command_provider: Some(lsp::ExecuteCommandOptions {
+            commands: vec![command_name.to_string()],
+            ..lsp::ExecuteCommandOptions::default()
+        }),
+        ..lsp::ServerCapabilities::default()
+    };
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
+        "Rust",
+        FakeLspAdapter {
+            capabilities: capabilities.clone(),
+            ..FakeLspAdapter::default()
+        },
+    );
+    client_b.language_registry().add(rust_lang());
+    client_b.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            capabilities,
+            ..FakeLspAdapter::default()
+        },
+    );
+
+    client_a
+        .fs()
+        .insert_tree(
+            path!("/dir"),
+            json!({
+                "one.rs": "const ONE: usize = 1;"
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+    let editor_b = workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
+        let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+        let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+        (lsp_store, buffer)
+    });
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    cx_a.run_until_parked();
+    cx_b.run_until_parked();
+
+    let long_request_time = LSP_REQUEST_TIMEOUT / 2;
+    let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
+    let requests_started = Arc::new(AtomicUsize::new(0));
+    let requests_completed = Arc::new(AtomicUsize::new(0));
+    let _lens_requests = fake_language_server
+        .set_request_handler::<lsp::request::CodeLensRequest, _, _>({
+            let request_started_tx = request_started_tx.clone();
+            let requests_started = requests_started.clone();
+            let requests_completed = requests_completed.clone();
+            move |params, cx| {
+                let mut request_started_tx = request_started_tx.clone();
+                let requests_started = requests_started.clone();
+                let requests_completed = requests_completed.clone();
+                async move {
+                    assert_eq!(
+                        params.text_document.uri.as_str(),
+                        uri!("file:///dir/one.rs")
+                    );
+                    requests_started.fetch_add(1, atomic::Ordering::Release);
+                    request_started_tx.send(()).await.unwrap();
+                    cx.background_executor().timer(long_request_time).await;
+                    let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
+                    Ok(Some(vec![lsp::CodeLens {
+                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
+                        command: Some(lsp::Command {
+                            title: format!("LSP Command {i}"),
+                            command: command_name.to_string(),
+                            arguments: None,
+                        }),
+                        data: None,
+                    }]))
+                }
+            }
+        });
+
+    // Move cursor to a location, this should trigger the code lens call.
+    editor_b.update_in(cx_b, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([7..7])
+        });
+    });
+    let () = request_started_rx.next().await.unwrap();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        1,
+        "Selection change should have initiated the first request"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        0,
+        "Slow requests should be running still"
+    );
+    let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
+        lsp_store
+            .forget_code_lens_task(buffer_b.read(cx).remote_id())
+            .expect("Should have the fetch task started")
+    });
+
+    editor_b.update_in(cx_b, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([1..1])
+        });
+    });
+    let () = request_started_rx.next().await.unwrap();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        2,
+        "Selection change should have initiated the second request"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        0,
+        "Slow requests should be running still"
+    );
+    let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
+        lsp_store
+            .forget_code_lens_task(buffer_b.read(cx).remote_id())
+            .expect("Should have the fetch task started for the 2nd time")
+    });
+
+    editor_b.update_in(cx_b, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([2..2])
+        });
+    });
+    let () = request_started_rx.next().await.unwrap();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        3,
+        "Selection change should have initiated the third request"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        0,
+        "Slow requests should be running still"
+    );
+
+    _first_task.await.unwrap();
+    _second_task.await.unwrap();
+    cx_b.run_until_parked();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        3,
+        "No selection changes should trigger no more code lens requests"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        3,
+        "After enough time, all 3 LSP requests should have been served by the language server"
+    );
+    let resulting_lens_actions = editor_b
+        .update(cx_b, |editor, cx| {
+            let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+            lsp_store.update(cx, |lsp_store, cx| {
+                lsp_store.code_lens_actions(&buffer_b, cx)
+            })
+        })
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(
+        resulting_lens_actions.len(),
+        1,
+        "Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
+    );
+    assert_eq!(
+        resulting_lens_actions.first().unwrap().lsp_action.title(),
+        "LSP Command 3",
+        "Only the final code lens action should be in the data"
+    )
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     let mut server = TestServer::start(cx_a.executor()).await;

crates/collab/src/tests/integration_tests.rs 🔗

@@ -4850,6 +4850,7 @@ async fn test_definition(
     let definitions_1 = project_b
         .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
         .await
+        .unwrap()
         .unwrap();
     cx_b.read(|cx| {
         assert_eq!(
@@ -4885,6 +4886,7 @@ async fn test_definition(
     let definitions_2 = project_b
         .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
         .await
+        .unwrap()
         .unwrap();
     cx_b.read(|cx| {
         assert_eq!(definitions_2.len(), 1);
@@ -4922,6 +4924,7 @@ async fn test_definition(
     let type_definitions = project_b
         .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
         .await
+        .unwrap()
         .unwrap();
     cx_b.read(|cx| {
         assert_eq!(
@@ -5060,7 +5063,7 @@ async fn test_references(
         ])))
         .unwrap();
 
-    let references = references.await.unwrap();
+    let references = references.await.unwrap().unwrap();
     executor.run_until_parked();
     project_b.read_with(cx_b, |project, cx| {
         // User is informed that a request is no longer pending.
@@ -5104,7 +5107,7 @@ async fn test_references(
     lsp_response_tx
         .unbounded_send(Err(anyhow!("can't find references")))
         .unwrap();
-    assert_eq!(references.await.unwrap(), []);
+    assert_eq!(references.await.unwrap().unwrap(), []);
 
     // User is informed that the request is no longer pending.
     executor.run_until_parked();
@@ -5505,7 +5508,8 @@ async fn test_lsp_hover(
     // Request hover information as the guest.
     let mut hovers = project_b
         .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
-        .await;
+        .await
+        .unwrap();
     assert_eq!(
         hovers.len(),
         2,
@@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
         definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
     }
 
-    let definitions = definitions.await.unwrap();
+    let definitions = definitions.await.unwrap().unwrap();
     assert_eq!(
         definitions.len(),
         1,

crates/editor/src/editor.rs 🔗

@@ -15710,7 +15710,9 @@ impl Editor {
         };
 
         cx.spawn_in(window, async move |editor, cx| {
-            let definitions = definitions.await?;
+            let Some(definitions) = definitions.await? else {
+                return Ok(Navigated::No);
+            };
             let navigated = editor
                 .update_in(cx, |editor, window, cx| {
                     editor.navigate_to_hover_links(
@@ -16052,7 +16054,9 @@ impl Editor {
                 }
             });
 
-            let locations = references.await?;
+            let Some(locations) = references.await? else {
+                return anyhow::Ok(Navigated::No);
+            };
             if locations.is_empty() {
                 return anyhow::Ok(Navigated::No);
             }
@@ -21837,7 +21841,7 @@ pub trait SemanticsProvider {
         buffer: &Entity<Buffer>,
         position: text::Anchor,
         cx: &mut App,
-    ) -> Option<Task<Vec<project::Hover>>>;
+    ) -> Option<Task<Option<Vec<project::Hover>>>>;
 
     fn inline_values(
         &self,
@@ -21876,7 +21880,7 @@ pub trait SemanticsProvider {
         position: text::Anchor,
         kind: GotoDefinitionKind,
         cx: &mut App,
-    ) -> Option<Task<Result<Vec<LocationLink>>>>;
+    ) -> Option<Task<Result<Option<Vec<LocationLink>>>>>;
 
     fn range_for_rename(
         &self,
@@ -21989,7 +21993,13 @@ impl CodeActionProvider for Entity<Project> {
                 Ok(code_lens_actions
                     .context("code lens fetch")?
                     .into_iter()
-                    .chain(code_actions.context("code action fetch")?)
+                    .flatten()
+                    .chain(
+                        code_actions
+                            .context("code action fetch")?
+                            .into_iter()
+                            .flatten(),
+                    )
                     .collect())
             })
         })
@@ -22284,7 +22294,7 @@ impl SemanticsProvider for Entity<Project> {
         buffer: &Entity<Buffer>,
         position: text::Anchor,
         cx: &mut App,
-    ) -> Option<Task<Vec<project::Hover>>> {
+    ) -> Option<Task<Option<Vec<project::Hover>>>> {
         Some(self.update(cx, |project, cx| project.hover(buffer, position, cx)))
     }
 
@@ -22305,7 +22315,7 @@ impl SemanticsProvider for Entity<Project> {
         position: text::Anchor,
         kind: GotoDefinitionKind,
         cx: &mut App,
-    ) -> Option<Task<Result<Vec<LocationLink>>>> {
+    ) -> Option<Task<Result<Option<Vec<LocationLink>>>>> {
         Some(self.update(cx, |project, cx| match kind {
             GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx),
             GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx),

crates/editor/src/hover_links.rs 🔗

@@ -559,7 +559,7 @@ pub fn show_link_definition(
                             provider.definitions(&buffer, buffer_position, preferred_kind, cx)
                         })?;
                         if let Some(task) = task {
-                            task.await.ok().map(|definition_result| {
+                            task.await.ok().flatten().map(|definition_result| {
                                 (
                                     definition_result.iter().find_map(|link| {
                                         link.origin.as_ref().and_then(|origin| {

crates/editor/src/hover_popover.rs 🔗

@@ -428,7 +428,7 @@ fn show_hover(
             };
 
             let hovers_response = if let Some(hover_request) = hover_request {
-                hover_request.await
+                hover_request.await.unwrap_or_default()
             } else {
                 Vec::new()
             };

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         buffer: &Entity<Buffer>,
         position: text::Anchor,
         cx: &mut App,
-    ) -> Option<Task<Vec<project::Hover>>> {
+    ) -> Option<Task<Option<Vec<project::Hover>>>> {
         let buffer = self.to_base(buffer, &[position], cx)?;
         self.0.hover(&buffer, position, cx)
     }
@@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         position: text::Anchor,
         kind: crate::GotoDefinitionKind,
         cx: &mut App,
-    ) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> {
+    ) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
         let buffer = self.to_base(buffer, &[position], cx)?;
         self.0.definitions(&buffer, position, kind, cx)
     }

crates/editor/src/signature_help.rs 🔗

@@ -182,7 +182,9 @@ impl Editor {
                 let signature_help = task.await;
                 editor
                     .update(cx, |editor, cx| {
-                        let Some(mut signature_help) = signature_help.into_iter().next() else {
+                        let Some(mut signature_help) =
+                            signature_help.unwrap_or_default().into_iter().next()
+                        else {
                             editor
                                 .signature_help_state
                                 .hide(SignatureHelpHiddenBy::AutoClose);

crates/lsp/src/lsp.rs 🔗

@@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
 const JSON_RPC_VERSION: &str = "2.0";
 const CONTENT_LEN_HEADER: &str = "Content-Length: ";
 
-const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
+pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
 const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
 
 type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;

crates/project/src/lsp_command.rs 🔗

@@ -3444,8 +3444,7 @@ impl LspCommand for GetCodeLens {
         capabilities
             .server_capabilities
             .code_lens_provider
-            .as_ref()
-            .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false))
+            .is_some()
     }
 
     fn to_lsp(

crates/project/src/lsp_store.rs 🔗

@@ -72,10 +72,11 @@ use lsp::{
     AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity,
     DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
     FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
-    LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId,
-    LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType,
-    OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles,
-    WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles,
+    LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
+    LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture,
+    MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
+    TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams,
+    WorkspaceFolder, notification::DidRenameFiles,
 };
 use node_runtime::read_package_installed_version;
 use parking_lot::Mutex;
@@ -84,7 +85,7 @@ use rand::prelude::*;
 
 use rpc::{
     AnyProtoClient,
-    proto::{FromProto, ToProto},
+    proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto},
 };
 use serde::Serialize;
 use settings::{Settings, SettingsLocation, SettingsStore};
@@ -92,7 +93,7 @@ use sha2::{Digest, Sha256};
 use smol::channel::Sender;
 use snippet::Snippet;
 use std::{
-    any::Any,
+    any::{Any, TypeId},
     borrow::Cow,
     cell::RefCell,
     cmp::{Ordering, Reverse},
@@ -3490,6 +3491,7 @@ pub struct LspStore {
     pub(super) lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
     lsp_document_colors: HashMap<BufferId, DocumentColorData>,
     lsp_code_lens: HashMap<BufferId, CodeLensData>,
+    running_lsp_requests: HashMap<TypeId, (Global, HashMap<LspRequestId, Task<()>>)>,
 }
 
 #[derive(Debug, Default, Clone)]
@@ -3499,7 +3501,7 @@ pub struct DocumentColors {
 }
 
 type DocumentColorTask = Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>;
-type CodeLensTask = Shared<Task<std::result::Result<Vec<CodeAction>, Arc<anyhow::Error>>>>;
+type CodeLensTask = Shared<Task<std::result::Result<Option<Vec<CodeAction>>, Arc<anyhow::Error>>>>;
 
 #[derive(Debug, Default)]
 struct DocumentColorData {
@@ -3579,6 +3581,8 @@ struct CoreSymbol {
 
 impl LspStore {
     pub fn init(client: &AnyProtoClient) {
+        client.add_entity_request_handler(Self::handle_lsp_query);
+        client.add_entity_message_handler(Self::handle_lsp_query_response);
         client.add_entity_request_handler(Self::handle_multi_lsp_query);
         client.add_entity_request_handler(Self::handle_restart_language_servers);
         client.add_entity_request_handler(Self::handle_stop_language_servers);
@@ -3758,6 +3762,7 @@ impl LspStore {
             lsp_server_capabilities: HashMap::default(),
             lsp_document_colors: HashMap::default(),
             lsp_code_lens: HashMap::default(),
+            running_lsp_requests: HashMap::default(),
             active_entry: None,
             _maintain_workspace_config,
             _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
@@ -3819,6 +3824,7 @@ impl LspStore {
             lsp_server_capabilities: HashMap::default(),
             lsp_document_colors: HashMap::default(),
             lsp_code_lens: HashMap::default(),
+            running_lsp_requests: HashMap::default(),
             active_entry: None,
 
             _maintain_workspace_config,
@@ -4381,8 +4387,6 @@ impl LspStore {
         }
     }
 
-    // TODO: remove MultiLspQuery: instead, the proto handler should pick appropriate server(s)
-    // Then, use `send_lsp_proto_request` or analogue for most of the LSP proto requests and inline this check inside
     fn is_capable_for_proto_request<R>(
         &self,
         buffer: &Entity<Buffer>,
@@ -5233,154 +5237,130 @@ impl LspStore {
 
     pub fn definitions(
         &mut self,
-        buffer_handle: &Entity<Buffer>,
+        buffer: &Entity<Buffer>,
         position: PointUtf16,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetDefinitions { position };
-            if !self.is_capable_for_proto_request(buffer_handle, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+            if !self.is_capable_for_proto_request(buffer, &request, cx) {
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer_handle.read(cx).remote_id().into(),
-                version: serialize_version(&buffer_handle.read(cx).version()),
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetDefinition(
-                    request.to_proto(project_id, buffer_handle.read(cx)),
-                )),
-            });
-            let buffer = buffer_handle.clone();
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
+            let buffer = buffer.clone();
             cx.spawn(async move |weak_project, cx| {
                 let Some(project) = weak_project.upgrade() else {
-                    return Ok(Vec::new());
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| match lsp_response.response? {
-                            proto::lsp_response::Response::GetDefinitionResponse(response) => {
-                                Some(response)
-                            }
-                            unexpected => {
-                                debug_panic!("Unexpected response: {unexpected:?}");
-                                None
-                            }
-                        })
-                        .map(|definitions_response| {
-                            GetDefinitions { position }.response_from_proto(
-                                definitions_response,
-                                project.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            )
-                        }),
-                )
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+                let actions = join_all(responses.payload.into_iter().map(|response| {
+                    GetDefinitions { position }.response_from_proto(
+                        response.response,
+                        project.clone(),
+                        buffer.clone(),
+                        cx.clone(),
+                    )
+                }))
                 .await;
 
-                Ok(actions
-                    .into_iter()
-                    .collect::<Result<Vec<Vec<_>>>>()?
-                    .into_iter()
-                    .flatten()
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    actions
+                        .into_iter()
+                        .collect::<Result<Vec<Vec<_>>>>()?
+                        .into_iter()
+                        .flatten()
+                        .dedup()
+                        .collect(),
+                ))
             })
         } else {
             let definitions_task = self.request_multiple_lsp_locally(
-                buffer_handle,
+                buffer,
                 Some(position),
                 GetDefinitions { position },
                 cx,
             );
             cx.background_spawn(async move {
-                Ok(definitions_task
-                    .await
-                    .into_iter()
-                    .flat_map(|(_, definitions)| definitions)
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    definitions_task
+                        .await
+                        .into_iter()
+                        .flat_map(|(_, definitions)| definitions)
+                        .dedup()
+                        .collect(),
+                ))
             })
         }
     }
 
     pub fn declarations(
         &mut self,
-        buffer_handle: &Entity<Buffer>,
+        buffer: &Entity<Buffer>,
         position: PointUtf16,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetDeclarations { position };
-            if !self.is_capable_for_proto_request(buffer_handle, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+            if !self.is_capable_for_proto_request(buffer, &request, cx) {
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer_handle.read(cx).remote_id().into(),
-                version: serialize_version(&buffer_handle.read(cx).version()),
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetDeclaration(
-                    request.to_proto(project_id, buffer_handle.read(cx)),
-                )),
-            });
-            let buffer = buffer_handle.clone();
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
+            let buffer = buffer.clone();
             cx.spawn(async move |weak_project, cx| {
                 let Some(project) = weak_project.upgrade() else {
-                    return Ok(Vec::new());
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| match lsp_response.response? {
-                            proto::lsp_response::Response::GetDeclarationResponse(response) => {
-                                Some(response)
-                            }
-                            unexpected => {
-                                debug_panic!("Unexpected response: {unexpected:?}");
-                                None
-                            }
-                        })
-                        .map(|declarations_response| {
-                            GetDeclarations { position }.response_from_proto(
-                                declarations_response,
-                                project.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            )
-                        }),
-                )
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+                let actions = join_all(responses.payload.into_iter().map(|response| {
+                    GetDeclarations { position }.response_from_proto(
+                        response.response,
+                        project.clone(),
+                        buffer.clone(),
+                        cx.clone(),
+                    )
+                }))
                 .await;
 
-                Ok(actions
-                    .into_iter()
-                    .collect::<Result<Vec<Vec<_>>>>()?
-                    .into_iter()
-                    .flatten()
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    actions
+                        .into_iter()
+                        .collect::<Result<Vec<Vec<_>>>>()?
+                        .into_iter()
+                        .flatten()
+                        .dedup()
+                        .collect(),
+                ))
             })
         } else {
             let declarations_task = self.request_multiple_lsp_locally(
-                buffer_handle,
+                buffer,
                 Some(position),
                 GetDeclarations { position },
                 cx,
             );
             cx.background_spawn(async move {
-                Ok(declarations_task
-                    .await
-                    .into_iter()
-                    .flat_map(|(_, declarations)| declarations)
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    declarations_task
+                        .await
+                        .into_iter()
+                        .flat_map(|(_, declarations)| declarations)
+                        .dedup()
+                        .collect(),
+                ))
             })
         }
     }
@@ -5390,59 +5370,45 @@ impl LspStore {
         buffer: &Entity<Buffer>,
         position: PointUtf16,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetTypeDefinitions { position };
             if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer.read(cx).remote_id().into(),
-                version: serialize_version(&buffer.read(cx).version()),
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetTypeDefinition(
-                    request.to_proto(project_id, buffer.read(cx)),
-                )),
-            });
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
             let buffer = buffer.clone();
             cx.spawn(async move |weak_project, cx| {
                 let Some(project) = weak_project.upgrade() else {
-                    return Ok(Vec::new());
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| match lsp_response.response? {
-                            proto::lsp_response::Response::GetTypeDefinitionResponse(response) => {
-                                Some(response)
-                            }
-                            unexpected => {
-                                debug_panic!("Unexpected response: {unexpected:?}");
-                                None
-                            }
-                        })
-                        .map(|type_definitions_response| {
-                            GetTypeDefinitions { position }.response_from_proto(
-                                type_definitions_response,
-                                project.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            )
-                        }),
-                )
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+                let actions = join_all(responses.payload.into_iter().map(|response| {
+                    GetTypeDefinitions { position }.response_from_proto(
+                        response.response,
+                        project.clone(),
+                        buffer.clone(),
+                        cx.clone(),
+                    )
+                }))
                 .await;
 
-                Ok(actions
-                    .into_iter()
-                    .collect::<Result<Vec<Vec<_>>>>()?
-                    .into_iter()
-                    .flatten()
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    actions
+                        .into_iter()
+                        .collect::<Result<Vec<Vec<_>>>>()?
+                        .into_iter()
+                        .flatten()
+                        .dedup()
+                        .collect(),
+                ))
             })
         } else {
             let type_definitions_task = self.request_multiple_lsp_locally(
@@ -5452,12 +5418,14 @@ impl LspStore {
                 cx,
             );
             cx.background_spawn(async move {
-                Ok(type_definitions_task
-                    .await
-                    .into_iter()
-                    .flat_map(|(_, type_definitions)| type_definitions)
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    type_definitions_task
+                        .await
+                        .into_iter()
+                        .flat_map(|(_, type_definitions)| type_definitions)
+                        .dedup()
+                        .collect(),
+                ))
             })
         }
     }
@@ -5467,59 +5435,45 @@ impl LspStore {
         buffer: &Entity<Buffer>,
         position: PointUtf16,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetImplementations { position };
             if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer.read(cx).remote_id().into(),
-                version: serialize_version(&buffer.read(cx).version()),
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetImplementation(
-                    request.to_proto(project_id, buffer.read(cx)),
-                )),
-            });
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
             let buffer = buffer.clone();
             cx.spawn(async move |weak_project, cx| {
                 let Some(project) = weak_project.upgrade() else {
-                    return Ok(Vec::new());
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| match lsp_response.response? {
-                            proto::lsp_response::Response::GetImplementationResponse(response) => {
-                                Some(response)
-                            }
-                            unexpected => {
-                                debug_panic!("Unexpected response: {unexpected:?}");
-                                None
-                            }
-                        })
-                        .map(|implementations_response| {
-                            GetImplementations { position }.response_from_proto(
-                                implementations_response,
-                                project.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            )
-                        }),
-                )
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+                let actions = join_all(responses.payload.into_iter().map(|response| {
+                    GetImplementations { position }.response_from_proto(
+                        response.response,
+                        project.clone(),
+                        buffer.clone(),
+                        cx.clone(),
+                    )
+                }))
                 .await;
 
-                Ok(actions
-                    .into_iter()
-                    .collect::<Result<Vec<Vec<_>>>>()?
-                    .into_iter()
-                    .flatten()
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    actions
+                        .into_iter()
+                        .collect::<Result<Vec<Vec<_>>>>()?
+                        .into_iter()
+                        .flatten()
+                        .dedup()
+                        .collect(),
+                ))
             })
         } else {
             let implementations_task = self.request_multiple_lsp_locally(
@@ -5529,12 +5483,14 @@ impl LspStore {
                 cx,
             );
             cx.background_spawn(async move {
-                Ok(implementations_task
-                    .await
-                    .into_iter()
-                    .flat_map(|(_, implementations)| implementations)
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    implementations_task
+                        .await
+                        .into_iter()
+                        .flat_map(|(_, implementations)| implementations)
+                        .dedup()
+                        .collect(),
+                ))
             })
         }
     }
@@ -5544,59 +5500,44 @@ impl LspStore {
         buffer: &Entity<Buffer>,
         position: PointUtf16,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Location>>> {
+    ) -> Task<Result<Option<Vec<Location>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetReferences { position };
             if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer.read(cx).remote_id().into(),
-                version: serialize_version(&buffer.read(cx).version()),
+
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetReferences(
-                    request.to_proto(project_id, buffer.read(cx)),
-                )),
-            });
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
             let buffer = buffer.clone();
             cx.spawn(async move |weak_project, cx| {
                 let Some(project) = weak_project.upgrade() else {
-                    return Ok(Vec::new());
+                    return Ok(None);
+                };
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| match lsp_response.response? {
-                            proto::lsp_response::Response::GetReferencesResponse(response) => {
-                                Some(response)
-                            }
-                            unexpected => {
-                                debug_panic!("Unexpected response: {unexpected:?}");
-                                None
-                            }
-                        })
-                        .map(|references_response| {
-                            GetReferences { position }.response_from_proto(
-                                references_response,
-                                project.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            )
-                        }),
-                )
-                .await;
 
-                Ok(actions
-                    .into_iter()
-                    .collect::<Result<Vec<Vec<_>>>>()?
-                    .into_iter()
-                    .flatten()
-                    .dedup()
-                    .collect())
+                let locations = join_all(responses.payload.into_iter().map(|lsp_response| {
+                    GetReferences { position }.response_from_proto(
+                        lsp_response.response,
+                        project.clone(),
+                        buffer.clone(),
+                        cx.clone(),
+                    )
+                }))
+                .await
+                .into_iter()
+                .collect::<Result<Vec<Vec<_>>>>()?
+                .into_iter()
+                .flatten()
+                .dedup()
+                .collect();
+                Ok(Some(locations))
             })
         } else {
             let references_task = self.request_multiple_lsp_locally(
@@ -5606,12 +5547,14 @@ impl LspStore {
                 cx,
             );
             cx.background_spawn(async move {
-                Ok(references_task
-                    .await
-                    .into_iter()
-                    .flat_map(|(_, references)| references)
-                    .dedup()
-                    .collect())
+                Ok(Some(
+                    references_task
+                        .await
+                        .into_iter()
+                        .flat_map(|(_, references)| references)
+                        .dedup()
+                        .collect(),
+                ))
             })
         }
     }
@@ -5622,65 +5565,51 @@ impl LspStore {
         range: Range<Anchor>,
         kinds: Option<Vec<CodeActionKind>>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<CodeAction>>> {
+    ) -> Task<Result<Option<Vec<CodeAction>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetCodeActions {
                 range: range.clone(),
                 kinds: kinds.clone(),
             };
             if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer.read(cx).remote_id().into(),
-                version: serialize_version(&buffer.read(cx).version()),
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetCodeActions(
-                    request.to_proto(project_id, buffer.read(cx)),
-                )),
-            });
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
             let buffer = buffer.clone();
             cx.spawn(async move |weak_project, cx| {
                 let Some(project) = weak_project.upgrade() else {
-                    return Ok(Vec::new());
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| match lsp_response.response? {
-                            proto::lsp_response::Response::GetCodeActionsResponse(response) => {
-                                Some(response)
-                            }
-                            unexpected => {
-                                debug_panic!("Unexpected response: {unexpected:?}");
-                                None
-                            }
-                        })
-                        .map(|code_actions_response| {
-                            GetCodeActions {
-                                range: range.clone(),
-                                kinds: kinds.clone(),
-                            }
-                            .response_from_proto(
-                                code_actions_response,
-                                project.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            )
-                        }),
-                )
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+                let actions = join_all(responses.payload.into_iter().map(|response| {
+                    GetCodeActions {
+                        range: range.clone(),
+                        kinds: kinds.clone(),
+                    }
+                    .response_from_proto(
+                        response.response,
+                        project.clone(),
+                        buffer.clone(),
+                        cx.clone(),
+                    )
+                }))
                 .await;
 
-                Ok(actions
-                    .into_iter()
-                    .collect::<Result<Vec<Vec<_>>>>()?
-                    .into_iter()
-                    .flatten()
-                    .collect())
+                Ok(Some(
+                    actions
+                        .into_iter()
+                        .collect::<Result<Vec<Vec<_>>>>()?
+                        .into_iter()
+                        .flatten()
+                        .collect(),
+                ))
             })
         } else {
             let all_actions_task = self.request_multiple_lsp_locally(
@@ -5690,11 +5619,13 @@ impl LspStore {
                 cx,
             );
             cx.background_spawn(async move {
-                Ok(all_actions_task
-                    .await
-                    .into_iter()
-                    .flat_map(|(_, actions)| actions)
-                    .collect())
+                Ok(Some(
+                    all_actions_task
+                        .await
+                        .into_iter()
+                        .flat_map(|(_, actions)| actions)
+                        .collect(),
+                ))
             })
         }
     }
@@ -5719,8 +5650,10 @@ impl LspStore {
                     != cached_data.lens.keys().copied().collect()
             });
             if !has_different_servers {
-                return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect()))
-                    .shared();
+                return Task::ready(Ok(Some(
+                    cached_data.lens.values().flatten().cloned().collect(),
+                )))
+                .shared();
             }
         }
 
@@ -5758,17 +5691,19 @@ impl LspStore {
                 lsp_store
                     .update(cx, |lsp_store, _| {
                         let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default();
-                        if lsp_data.lens_for_version == query_version_queried_for {
-                            lsp_data.lens.extend(fetched_lens.clone());
-                        } else if !lsp_data
-                            .lens_for_version
-                            .changed_since(&query_version_queried_for)
-                        {
-                            lsp_data.lens_for_version = query_version_queried_for;
-                            lsp_data.lens = fetched_lens.clone();
+                        if let Some(fetched_lens) = fetched_lens {
+                            if lsp_data.lens_for_version == query_version_queried_for {
+                                lsp_data.lens.extend(fetched_lens.clone());
+                            } else if !lsp_data
+                                .lens_for_version
+                                .changed_since(&query_version_queried_for)
+                            {
+                                lsp_data.lens_for_version = query_version_queried_for;
+                                lsp_data.lens = fetched_lens.clone();
+                            }
                         }
                         lsp_data.update = None;
-                        lsp_data.lens.values().flatten().cloned().collect()
+                        Some(lsp_data.lens.values().flatten().cloned().collect())
                     })
                     .map_err(Arc::new)
             })
@@ -5781,64 +5716,40 @@ impl LspStore {
         &mut self,
         buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<HashMap<LanguageServerId, Vec<CodeAction>>>> {
+    ) -> Task<Result<Option<HashMap<LanguageServerId, Vec<CodeAction>>>>> {
         if let Some((upstream_client, project_id)) = self.upstream_client() {
             let request = GetCodeLens;
             if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(HashMap::default()));
+                return Task::ready(Ok(None));
             }
-            let request_task = upstream_client.request(proto::MultiLspQuery {
-                buffer_id: buffer.read(cx).remote_id().into(),
-                version: serialize_version(&buffer.read(cx).version()),
+            let request_task = upstream_client.request_lsp(
                 project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetCodeLens(
-                    request.to_proto(project_id, buffer.read(cx)),
-                )),
-            });
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
             let buffer = buffer.clone();
             cx.spawn(async move |weak_lsp_store, cx| {
                 let Some(lsp_store) = weak_lsp_store.upgrade() else {
-                    return Ok(HashMap::default());
+                    return Ok(None);
                 };
-                let responses = request_task.await?.responses;
-                let code_lens_actions = join_all(
-                    responses
-                        .into_iter()
-                        .filter_map(|lsp_response| {
-                            let response = match lsp_response.response? {
-                                proto::lsp_response::Response::GetCodeLensResponse(response) => {
-                                    Some(response)
-                                }
-                                unexpected => {
-                                    debug_panic!("Unexpected response: {unexpected:?}");
-                                    None
-                                }
-                            }?;
-                            let server_id = LanguageServerId::from_proto(lsp_response.server_id);
-                            Some((server_id, response))
-                        })
-                        .map(|(server_id, code_lens_response)| {
-                            let lsp_store = lsp_store.clone();
-                            let buffer = buffer.clone();
-                            let cx = cx.clone();
-                            async move {
-                                (
-                                    server_id,
-                                    GetCodeLens
-                                        .response_from_proto(
-                                            code_lens_response,
-                                            lsp_store,
-                                            buffer,
-                                            cx,
-                                        )
-                                        .await,
-                                )
-                            }
-                        }),
-                )
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+
+                let code_lens_actions = join_all(responses.payload.into_iter().map(|response| {
+                    let lsp_store = lsp_store.clone();
+                    let buffer = buffer.clone();
+                    let cx = cx.clone();
+                    async move {
+                        (
+                            LanguageServerId::from_proto(response.server_id),
+                            GetCodeLens
+                                .response_from_proto(response.response, lsp_store, buffer, cx)
+                                .await,
+                        )
+                    }
+                }))
                 .await;
 
                 let mut has_errors = false;
@@ -5857,14 +5768,14 @@ impl LspStore {
                     !has_errors || !code_lens_actions.is_empty(),
                     "Failed to fetch code lens"
                 );
-                Ok(code_lens_actions)
+                Ok(Some(code_lens_actions))
             })
         } else {
             let code_lens_actions_task =
                 self.request_multiple_lsp_locally(buffer, None::<usize>, GetCodeLens, cx);
-            cx.background_spawn(
-                async move { Ok(code_lens_actions_task.await.into_iter().collect()) },
-            )
+            cx.background_spawn(async move {
+                Ok(Some(code_lens_actions_task.await.into_iter().collect()))
+            })
         }
     }
 
@@ -6480,48 +6391,23 @@ impl LspStore {
         let buffer_id = buffer.read(cx).remote_id();
 
         if let Some((client, upstream_project_id)) = self.upstream_client() {
-            if !self.is_capable_for_proto_request(
-                &buffer,
-                &GetDocumentDiagnostics {
-                    previous_result_id: None,
-                },
-                cx,
-            ) {
+            let request = GetDocumentDiagnostics {
+                previous_result_id: None,
+            };
+            if !self.is_capable_for_proto_request(&buffer, &request, cx) {
                 return Task::ready(Ok(None));
             }
-            let request_task = client.request(proto::MultiLspQuery {
-                buffer_id: buffer_id.to_proto(),
-                version: serialize_version(&buffer.read(cx).version()),
-                project_id: upstream_project_id,
-                strategy: Some(proto::multi_lsp_query::Strategy::All(
-                    proto::AllLanguageServers {},
-                )),
-                request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
-                    proto::GetDocumentDiagnostics {
-                        project_id: upstream_project_id,
-                        buffer_id: buffer_id.to_proto(),
-                        version: serialize_version(&buffer.read(cx).version()),
-                    },
-                )),
-            });
+            let request_task = client.request_lsp(
+                upstream_project_id,
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(upstream_project_id, buffer.read(cx)),
+            );
             cx.background_spawn(async move {
-                let _proto_responses = request_task
-                    .await?
-                    .responses
-                    .into_iter()
-                    .filter_map(|lsp_response| match lsp_response.response? {
-                        proto::lsp_response::Response::GetDocumentDiagnosticsResponse(response) => {
-                            Some(response)
-                        }
-                        unexpected => {
-                            debug_panic!("Unexpected response: {unexpected:?}");
-                            None
-                        }
-                    })
-                    .collect::<Vec<_>>();
                 // Proto requests cause the diagnostics to be pulled from language server(s) on the local side
                 // and then, buffer state updated with the diagnostics received, which will be later propagated to the client.
                 // Do not attempt to further process the dummy responses here.
+                let _response = request_task.await?;
                 Ok(None)
             })
         } else {
@@ -6806,16 +6692,18 @@ impl LspStore {
                     .update(cx, |lsp_store, _| {
                         let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default();
 
-                        if lsp_data.colors_for_version == query_version_queried_for {
-                            lsp_data.colors.extend(fetched_colors.clone());
-                            lsp_data.cache_version += 1;
-                        } else if !lsp_data
-                            .colors_for_version
-                            .changed_since(&query_version_queried_for)
-                        {
-                            lsp_data.colors_for_version = query_version_queried_for;
-                            lsp_data.colors = fetched_colors.clone();
-                            lsp_data.cache_version += 1;
+                        if let Some(fetched_colors) = fetched_colors {
+                            if lsp_data.colors_for_version == query_version_queried_for {
+                                lsp_data.colors.extend(fetched_colors.clone());
+                                lsp_data.cache_version += 1;
+                            } else if !lsp_data
+                                .colors_for_version
+                                .changed_since(&query_version_queried_for)
+                            {
+                                lsp_data.colors_for_version = query_version_queried_for;
+                                lsp_data.colors = fetched_colors.clone();
+                                lsp_data.cache_version += 1;
+                            }
                         }
                         lsp_data.colors_update = None;
                         let colors = lsp_data

crates/project/src/project.rs 🔗

@@ -3415,7 +3415,7 @@ impl Project {
         buffer: &Entity<Buffer>,
         position: T,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         let guard = self.retain_remotely_created_models(cx);
         let task = self.lsp_store.update(cx, |lsp_store, cx| {
@@ -3433,7 +3433,7 @@ impl Project {
         buffer: &Entity<Buffer>,
         position: T,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         let guard = self.retain_remotely_created_models(cx);
         let task = self.lsp_store.update(cx, |lsp_store, cx| {
@@ -3451,7 +3451,7 @@ impl Project {
         buffer: &Entity<Buffer>,
         position: T,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         let guard = self.retain_remotely_created_models(cx);
         let task = self.lsp_store.update(cx, |lsp_store, cx| {
@@ -3469,7 +3469,7 @@ impl Project {
         buffer: &Entity<Buffer>,
         position: T,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LocationLink>>> {
+    ) -> Task<Result<Option<Vec<LocationLink>>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         let guard = self.retain_remotely_created_models(cx);
         let task = self.lsp_store.update(cx, |lsp_store, cx| {
@@ -3487,7 +3487,7 @@ impl Project {
         buffer: &Entity<Buffer>,
         position: T,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Location>>> {
+    ) -> Task<Result<Option<Vec<Location>>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         let guard = self.retain_remotely_created_models(cx);
         let task = self.lsp_store.update(cx, |lsp_store, cx| {
@@ -3585,23 +3585,12 @@ impl Project {
         })
     }
 
-    pub fn signature_help<T: ToPointUtf16>(
-        &self,
-        buffer: &Entity<Buffer>,
-        position: T,
-        cx: &mut Context<Self>,
-    ) -> Task<Vec<SignatureHelp>> {
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.signature_help(buffer, position, cx)
-        })
-    }
-
     pub fn hover<T: ToPointUtf16>(
         &self,
         buffer: &Entity<Buffer>,
         position: T,
         cx: &mut Context<Self>,
-    ) -> Task<Vec<Hover>> {
+    ) -> Task<Option<Vec<Hover>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         self.lsp_store
             .update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx))
@@ -3637,7 +3626,7 @@ impl Project {
         range: Range<T>,
         kinds: Option<Vec<CodeActionKind>>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<CodeAction>>> {
+    ) -> Task<Result<Option<Vec<CodeAction>>>> {
         let buffer = buffer_handle.read(cx);
         let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
         self.lsp_store.update(cx, |lsp_store, cx| {
@@ -3650,7 +3639,7 @@ impl Project {
         buffer: &Entity<Buffer>,
         range: Range<T>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<CodeAction>>> {
+    ) -> Task<Result<Option<Vec<CodeAction>>>> {
         let snapshot = buffer.read(cx).snapshot();
         let range = range.to_point(&snapshot);
         let range_start = snapshot.anchor_before(range.start);
@@ -3668,16 +3657,18 @@ impl Project {
             let mut code_lens_actions = code_lens_actions
                 .await
                 .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?;
-            code_lens_actions.retain(|code_lens_action| {
-                range
-                    .start
-                    .cmp(&code_lens_action.range.start, &snapshot)
-                    .is_ge()
-                    && range
-                        .end
-                        .cmp(&code_lens_action.range.end, &snapshot)
-                        .is_le()
-            });
+            if let Some(code_lens_actions) = &mut code_lens_actions {
+                code_lens_actions.retain(|code_lens_action| {
+                    range
+                        .start
+                        .cmp(&code_lens_action.range.start, &snapshot)
+                        .is_ge()
+                        && range
+                            .end
+                            .cmp(&code_lens_action.range.end, &snapshot)
+                            .is_le()
+                });
+            }
             Ok(code_lens_actions)
         })
     }

crates/project/src/project_tests.rs 🔗

@@ -3005,6 +3005,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
     let mut definitions = project
         .update(cx, |project, cx| project.definitions(&buffer, 22, cx))
         .await
+        .unwrap()
         .unwrap();
 
     // Assert no new language server started
@@ -3519,7 +3520,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
         .next()
         .await;
 
-    let action = actions.await.unwrap()[0].clone();
+    let action = actions.await.unwrap().unwrap()[0].clone();
     let apply = project.update(cx, |project, cx| {
         project.apply_code_action(buffer.clone(), action, true, cx)
     });
@@ -6110,6 +6111,7 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
         hover_task
             .await
             .into_iter()
+            .flatten()
             .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
             .sorted()
             .collect::<Vec<_>>(),
@@ -6183,6 +6185,7 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
         hover_task
             .await
             .into_iter()
+            .flatten()
             .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
             .sorted()
             .collect::<Vec<_>>(),
@@ -6261,7 +6264,7 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
         .await
         .expect("The code action request should have been triggered");
 
-    let code_actions = code_actions_task.await.unwrap();
+    let code_actions = code_actions_task.await.unwrap().unwrap();
     assert_eq!(code_actions.len(), 1);
     assert_eq!(
         code_actions[0].lsp_action.action_kind(),
@@ -6420,6 +6423,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
         code_actions_task
             .await
             .unwrap()
+            .unwrap()
             .into_iter()
             .map(|code_action| code_action.lsp_action.title().to_owned())
             .sorted()

crates/proto/proto/lsp.proto 🔗

@@ -753,26 +753,45 @@ message TextEdit {
     PointUtf16 lsp_range_end = 3;
 }
 
-message MultiLspQuery {
+message LspQuery {
     uint64 project_id = 1;
-    uint64 buffer_id = 2;
-    repeated VectorClockEntry version = 3;
-    oneof strategy {
-        AllLanguageServers all = 4;
-    }
+    uint64 lsp_request_id = 2;
     oneof request {
+        GetReferences get_references = 3;
+        GetDocumentColor get_document_color = 4;
         GetHover get_hover = 5;
         GetCodeActions get_code_actions = 6;
         GetSignatureHelp get_signature_help = 7;
         GetCodeLens get_code_lens = 8;
         GetDocumentDiagnostics get_document_diagnostics = 9;
-        GetDocumentColor get_document_color = 10;
-        GetDefinition get_definition = 11;
-        GetDeclaration get_declaration = 12;
-        GetTypeDefinition get_type_definition = 13;
-        GetImplementation get_implementation = 14;
-        GetReferences get_references = 15;
+        GetDefinition get_definition = 10;
+        GetDeclaration get_declaration = 11;
+        GetTypeDefinition get_type_definition = 12;
+        GetImplementation get_implementation = 13;
+    }
+}
+
+message LspQueryResponse {
+    uint64 project_id = 1;
+    uint64 lsp_request_id = 2;
+    repeated LspResponse responses = 3;
+}
+
+message LspResponse {
+    oneof response {
+        GetHoverResponse get_hover_response = 1;
+        GetCodeActionsResponse get_code_actions_response = 2;
+        GetSignatureHelpResponse get_signature_help_response = 3;
+        GetCodeLensResponse get_code_lens_response = 4;
+        GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
+        GetDocumentColorResponse get_document_color_response = 6;
+        GetDefinitionResponse get_definition_response = 8;
+        GetDeclarationResponse get_declaration_response = 9;
+        GetTypeDefinitionResponse get_type_definition_response = 10;
+        GetImplementationResponse get_implementation_response = 11;
+        GetReferencesResponse get_references_response = 12;
     }
+    uint64 server_id = 7;
 }
 
 message AllLanguageServers {}
@@ -798,27 +817,6 @@ message StopLanguageServers {
     bool all = 4;
 }
 
-message MultiLspQueryResponse {
-    repeated LspResponse responses = 1;
-}
-
-message LspResponse {
-    oneof response {
-        GetHoverResponse get_hover_response = 1;
-        GetCodeActionsResponse get_code_actions_response = 2;
-        GetSignatureHelpResponse get_signature_help_response = 3;
-        GetCodeLensResponse get_code_lens_response = 4;
-        GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
-        GetDocumentColorResponse get_document_color_response = 6;
-        GetDefinitionResponse get_definition_response = 8;
-        GetDeclarationResponse get_declaration_response = 9;
-        GetTypeDefinitionResponse get_type_definition_response = 10;
-        GetImplementationResponse get_implementation_response = 11;
-        GetReferencesResponse get_references_response = 12;
-    }
-    uint64 server_id = 7;
-}
-
 message LspExtRunnables {
     uint64 project_id = 1;
     uint64 buffer_id = 2;
@@ -909,3 +907,30 @@ message PullWorkspaceDiagnostics {
     uint64 project_id = 1;
     uint64 server_id = 2;
 }
+
+// todo(lsp) remove after Zed Stable hits v0.204.x
+message MultiLspQuery {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated VectorClockEntry version = 3;
+    oneof strategy {
+        AllLanguageServers all = 4;
+    }
+    oneof request {
+        GetHover get_hover = 5;
+        GetCodeActions get_code_actions = 6;
+        GetSignatureHelp get_signature_help = 7;
+        GetCodeLens get_code_lens = 8;
+        GetDocumentDiagnostics get_document_diagnostics = 9;
+        GetDocumentColor get_document_color = 10;
+        GetDefinition get_definition = 11;
+        GetDeclaration get_declaration = 12;
+        GetTypeDefinition get_type_definition = 13;
+        GetImplementation get_implementation = 14;
+        GetReferences get_references = 15;
+    }
+}
+
+message MultiLspQueryResponse {
+    repeated LspResponse responses = 1;
+}

crates/proto/proto/zed.proto 🔗

@@ -393,7 +393,10 @@ message Envelope {
         GetCrashFilesResponse get_crash_files_response = 362;
 
         GitClone git_clone = 363;
-        GitCloneResponse git_clone_response = 364; // current max
+        GitCloneResponse git_clone_response = 364;
+
+        LspQuery lsp_query = 365;
+        LspQueryResponse lsp_query_response = 366; // current max
     }
 
     reserved 87 to 88;

crates/proto/src/macros.rs 🔗

@@ -69,3 +69,32 @@ macro_rules! entity_messages {
         })*
     };
 }
+
+#[macro_export]
+macro_rules! lsp_messages {
+    ($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => {
+        $(impl LspRequestMessage for $request_name {
+            type Response = $response_name;
+
+            fn to_proto_query(self) -> $crate::lsp_query::Request {
+                $crate::lsp_query::Request::$request_name(self)
+            }
+
+            fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response {
+                $crate::lsp_response::Response::$response_name(response)
+            }
+
+            fn buffer_id(&self) -> u64 {
+                self.buffer_id
+            }
+
+            fn buffer_version(&self) -> &[$crate::VectorClockEntry] {
+                &self.version
+            }
+
+            fn stop_previous_requests() -> bool {
+                $stop_previous_requests
+            }
+        })*
+    };
+}

crates/proto/src/proto.rs 🔗

@@ -169,6 +169,9 @@ messages!(
     (MarkNotificationRead, Foreground),
     (MoveChannel, Foreground),
     (ReorderChannel, Foreground),
+    (LspQuery, Background),
+    (LspQueryResponse, Background),
+    // todo(lsp) remove after Zed Stable hits v0.204.x
     (MultiLspQuery, Background),
     (MultiLspQueryResponse, Background),
     (OnTypeFormatting, Background),
@@ -426,7 +429,10 @@ request_messages!(
     (SetRoomParticipantRole, Ack),
     (BlameBuffer, BlameBufferResponse),
     (RejoinRemoteProjects, RejoinRemoteProjectsResponse),
+    // todo(lsp) remove after Zed Stable hits v0.204.x
     (MultiLspQuery, MultiLspQueryResponse),
+    (LspQuery, Ack),
+    (LspQueryResponse, Ack),
     (RestartLanguageServers, Ack),
     (StopLanguageServers, Ack),
     (OpenContext, OpenContextResponse),
@@ -478,6 +484,20 @@ request_messages!(
     (GitClone, GitCloneResponse)
 );
 
+lsp_messages!(
+    (GetReferences, GetReferencesResponse, true),
+    (GetDocumentColor, GetDocumentColorResponse, true),
+    (GetHover, GetHoverResponse, true),
+    (GetCodeActions, GetCodeActionsResponse, true),
+    (GetSignatureHelp, GetSignatureHelpResponse, true),
+    (GetCodeLens, GetCodeLensResponse, true),
+    (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true),
+    (GetDefinition, GetDefinitionResponse, true),
+    (GetDeclaration, GetDeclarationResponse, true),
+    (GetTypeDefinition, GetTypeDefinitionResponse, true),
+    (GetImplementation, GetImplementationResponse, true),
+);
+
 entity_messages!(
     {project_id, ShareProject},
     AddProjectCollaborator,
@@ -520,6 +540,9 @@ entity_messages!(
     LeaveProject,
     LinkedEditingRange,
     LoadCommitDiff,
+    LspQuery,
+    LspQueryResponse,
+    // todo(lsp) remove after Zed Stable hits v0.204.x
     MultiLspQuery,
     RestartLanguageServers,
     StopLanguageServers,
@@ -777,6 +800,28 @@ pub fn split_repository_update(
     }])
 }
 
+impl LspQuery {
+    pub fn query_name_and_write_permissions(&self) -> (&str, bool) {
+        match self.request {
+            Some(lsp_query::Request::GetHover(_)) => ("GetHover", false),
+            Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true),
+            Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false),
+            Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true),
+            Some(lsp_query::Request::GetDocumentDiagnostics(_)) => {
+                ("GetDocumentDiagnostics", false)
+            }
+            Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false),
+            Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false),
+            Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false),
+            Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false),
+            Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false),
+            Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false),
+            None => ("<unknown>", true),
+        }
+    }
+}
+
+// todo(lsp) remove after Zed Stable hits v0.204.x
 impl MultiLspQuery {
     pub fn request_str(&self) -> &str {
         match self.request {

crates/proto/src/typed_envelope.rs 🔗

@@ -31,6 +31,58 @@ pub trait RequestMessage: EnvelopedMessage {
     type Response: EnvelopedMessage;
 }
 
+/// A trait to bind LSP request and responses for the proto layer.
+/// Should be used for every LSP request that has to traverse through the proto layer.
+///
+/// `lsp_messages` macro in the same crate provides a convenient way to implement this.
+pub trait LspRequestMessage: EnvelopedMessage {
+    type Response: EnvelopedMessage;
+
+    fn to_proto_query(self) -> crate::lsp_query::Request;
+
+    fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response;
+
+    fn buffer_id(&self) -> u64;
+
+    fn buffer_version(&self) -> &[crate::VectorClockEntry];
+
+    /// Whether to deduplicate the requests, or keep the previous ones running when another
+    /// request of the same kind is processed.
+    fn stop_previous_requests() -> bool;
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct LspRequestId(pub u64);
+
+/// A response from a single language server.
+/// There could be multiple responses for a single LSP request,
+/// from different servers.
+pub struct ProtoLspResponse<R> {
+    pub server_id: u64,
+    pub response: R,
+}
+
+impl ProtoLspResponse<Box<dyn AnyTypedEnvelope>> {
+    pub fn into_response<T: LspRequestMessage>(self) -> Result<ProtoLspResponse<T::Response>> {
+        let envelope = self
+            .response
+            .into_any()
+            .downcast::<TypedEnvelope<T::Response>>()
+            .map_err(|_| {
+                anyhow::anyhow!(
+                    "cannot downcast LspResponse to {} for message {}",
+                    T::Response::NAME,
+                    T::NAME,
+                )
+            })?;
+
+        Ok(ProtoLspResponse {
+            server_id: self.server_id,
+            response: envelope.payload,
+        })
+    }
+}
+
 pub trait AnyTypedEnvelope: Any + Send + Sync {
     fn payload_type_id(&self) -> TypeId;
     fn payload_type_name(&self) -> &'static str;

crates/rpc/src/proto_client.rs 🔗

@@ -1,35 +1,48 @@
-use anyhow::Context;
+use anyhow::{Context, Result};
 use collections::HashMap;
 use futures::{
     Future, FutureExt as _,
+    channel::oneshot,
     future::{BoxFuture, LocalBoxFuture},
 };
-use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, Entity};
+use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _};
+use parking_lot::Mutex;
 use proto::{
-    AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, RequestMessage, TypedEnvelope,
-    error::ErrorExt as _,
+    AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage,
+    RequestMessage, TypedEnvelope, error::ErrorExt as _,
 };
 use std::{
     any::{Any, TypeId},
-    sync::{Arc, Weak},
+    sync::{
+        Arc, OnceLock,
+        atomic::{self, AtomicU64},
+    },
+    time::Duration,
 };
 
 #[derive(Clone)]
-pub struct AnyProtoClient(Arc<dyn ProtoClient>);
+pub struct AnyProtoClient(Arc<State>);
 
-impl AnyProtoClient {
-    pub fn downgrade(&self) -> AnyWeakProtoClient {
-        AnyWeakProtoClient(Arc::downgrade(&self.0))
-    }
-}
+type RequestIds = Arc<
+    Mutex<
+        HashMap<
+            LspRequestId,
+            oneshot::Sender<
+                Result<
+                    Option<TypedEnvelope<Vec<proto::ProtoLspResponse<Box<dyn AnyTypedEnvelope>>>>>,
+                >,
+            >,
+        >,
+    >,
+>;
 
-#[derive(Clone)]
-pub struct AnyWeakProtoClient(Weak<dyn ProtoClient>);
+static NEXT_LSP_REQUEST_ID: OnceLock<Arc<AtomicU64>> = OnceLock::new();
+static REQUEST_IDS: OnceLock<RequestIds> = OnceLock::new();
 
-impl AnyWeakProtoClient {
-    pub fn upgrade(&self) -> Option<AnyProtoClient> {
-        self.0.upgrade().map(AnyProtoClient)
-    }
+struct State {
+    client: Arc<dyn ProtoClient>,
+    next_lsp_request_id: Arc<AtomicU64>,
+    request_ids: RequestIds,
 }
 
 pub trait ProtoClient: Send + Sync {
@@ -37,11 +50,11 @@ pub trait ProtoClient: Send + Sync {
         &self,
         envelope: Envelope,
         request_type: &'static str,
-    ) -> BoxFuture<'static, anyhow::Result<Envelope>>;
+    ) -> BoxFuture<'static, Result<Envelope>>;
 
-    fn send(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>;
+    fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>;
 
-    fn send_response(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>;
+    fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>;
 
     fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet>;
 
@@ -65,7 +78,7 @@ pub type ProtoMessageHandler = Arc<
             Box<dyn AnyTypedEnvelope>,
             AnyProtoClient,
             AsyncApp,
-        ) -> LocalBoxFuture<'static, anyhow::Result<()>>,
+        ) -> LocalBoxFuture<'static, Result<()>>,
 >;
 
 impl ProtoMessageHandlerSet {
@@ -113,7 +126,7 @@ impl ProtoMessageHandlerSet {
         message: Box<dyn AnyTypedEnvelope>,
         client: AnyProtoClient,
         cx: AsyncApp,
-    ) -> Option<LocalBoxFuture<'static, anyhow::Result<()>>> {
+    ) -> Option<LocalBoxFuture<'static, Result<()>>> {
         let payload_type_id = message.payload_type_id();
         let mut this = this.lock();
         let handler = this.message_handlers.get(&payload_type_id)?.clone();
@@ -169,43 +182,195 @@ where
     T: ProtoClient + 'static,
 {
     fn from(client: Arc<T>) -> Self {
-        Self(client)
+        Self::new(client)
     }
 }
 
 impl AnyProtoClient {
     pub fn new<T: ProtoClient + 'static>(client: Arc<T>) -> Self {
-        Self(client)
+        Self(Arc::new(State {
+            client,
+            next_lsp_request_id: NEXT_LSP_REQUEST_ID
+                .get_or_init(|| Arc::new(AtomicU64::new(0)))
+                .clone(),
+            request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(),
+        }))
     }
 
     pub fn is_via_collab(&self) -> bool {
-        self.0.is_via_collab()
+        self.0.client.is_via_collab()
     }
 
     pub fn request<T: RequestMessage>(
         &self,
         request: T,
-    ) -> impl Future<Output = anyhow::Result<T::Response>> + use<T> {
+    ) -> impl Future<Output = Result<T::Response>> + use<T> {
         let envelope = request.into_envelope(0, None, None);
-        let response = self.0.request(envelope, T::NAME);
+        let response = self.0.client.request(envelope, T::NAME);
         async move {
             T::Response::from_envelope(response.await?)
                 .context("received response of the wrong type")
         }
     }
 
-    pub fn send<T: EnvelopedMessage>(&self, request: T) -> anyhow::Result<()> {
+    pub fn send<T: EnvelopedMessage>(&self, request: T) -> Result<()> {
         let envelope = request.into_envelope(0, None, None);
-        self.0.send(envelope, T::NAME)
+        self.0.client.send(envelope, T::NAME)
+    }
+
+    pub fn send_response<T: EnvelopedMessage>(&self, request_id: u32, request: T) -> Result<()> {
+        let envelope = request.into_envelope(0, Some(request_id), None);
+        self.0.client.send(envelope, T::NAME)
     }
 
-    pub fn send_response<T: EnvelopedMessage>(
+    pub fn request_lsp<T>(
         &self,
-        request_id: u32,
+        project_id: u64,
+        timeout: Duration,
+        executor: BackgroundExecutor,
         request: T,
-    ) -> anyhow::Result<()> {
-        let envelope = request.into_envelope(0, Some(request_id), None);
-        self.0.send(envelope, T::NAME)
+    ) -> impl Future<
+        Output = Result<Option<TypedEnvelope<Vec<proto::ProtoLspResponse<T::Response>>>>>,
+    > + use<T>
+    where
+        T: LspRequestMessage,
+    {
+        let new_id = LspRequestId(
+            self.0
+                .next_lsp_request_id
+                .fetch_add(1, atomic::Ordering::Acquire),
+        );
+        let (tx, rx) = oneshot::channel();
+        {
+            self.0.request_ids.lock().insert(new_id, tx);
+        }
+
+        let query = proto::LspQuery {
+            project_id,
+            lsp_request_id: new_id.0,
+            request: Some(request.clone().to_proto_query()),
+        };
+        let request = self.request(query);
+        let request_ids = self.0.request_ids.clone();
+        async move {
+            match request.await {
+                Ok(_request_enqueued) => {}
+                Err(e) => {
+                    request_ids.lock().remove(&new_id);
+                    return Err(e).context("sending LSP proto request");
+                }
+            }
+
+            let response = rx.with_timeout(timeout, &executor).await;
+            {
+                request_ids.lock().remove(&new_id);
+            }
+            match response {
+                Ok(Ok(response)) => {
+                    let response = response
+                        .context("waiting for LSP proto response")?
+                        .map(|response| {
+                            anyhow::Ok(TypedEnvelope {
+                                payload: response
+                                    .payload
+                                    .into_iter()
+                                    .map(|lsp_response| lsp_response.into_response::<T>())
+                                    .collect::<Result<Vec<_>>>()?,
+                                sender_id: response.sender_id,
+                                original_sender_id: response.original_sender_id,
+                                message_id: response.message_id,
+                                received_at: response.received_at,
+                            })
+                        })
+                        .transpose()
+                        .context("converting LSP proto response")?;
+                    Ok(response)
+                }
+                Err(_cancelled_due_timeout) => Ok(None),
+                Ok(Err(_channel_dropped)) => Ok(None),
+            }
+        }
+    }
+
+    pub fn send_lsp_response<T: LspRequestMessage>(
+        &self,
+        project_id: u64,
+        lsp_request_id: LspRequestId,
+        server_responses: HashMap<u64, T::Response>,
+    ) -> Result<()> {
+        self.send(proto::LspQueryResponse {
+            project_id,
+            lsp_request_id: lsp_request_id.0,
+            responses: server_responses
+                .into_iter()
+                .map(|(server_id, response)| proto::LspResponse {
+                    server_id,
+                    response: Some(T::response_to_proto_query(response)),
+                })
+                .collect(),
+        })
+    }
+
+    pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope<proto::LspQueryResponse>) {
+        let request_id = LspRequestId(envelope.payload.lsp_request_id);
+        let mut response_senders = self.0.request_ids.lock();
+        if let Some(tx) = response_senders.remove(&request_id) {
+            let responses = envelope.payload.responses.drain(..).collect::<Vec<_>>();
+            tx.send(Ok(Some(proto::TypedEnvelope {
+                sender_id: envelope.sender_id,
+                original_sender_id: envelope.original_sender_id,
+                message_id: envelope.message_id,
+                received_at: envelope.received_at,
+                payload: responses
+                    .into_iter()
+                    .filter_map(|response| {
+                        use proto::lsp_response::Response;
+
+                        let server_id = response.server_id;
+                        let response = match response.response? {
+                            Response::GetReferencesResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetDocumentColorResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetHoverResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetCodeActionsResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetSignatureHelpResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetCodeLensResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetDocumentDiagnosticsResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetDefinitionResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetDeclarationResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetTypeDefinitionResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                            Response::GetImplementationResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
+                        };
+                        Some(proto::ProtoLspResponse {
+                            server_id,
+                            response,
+                        })
+                    })
+                    .collect(),
+            })))
+            .ok();
+        }
     }
 
     pub fn add_request_handler<M, E, H, F>(&self, entity: gpui::WeakEntity<E>, handler: H)
@@ -213,31 +378,35 @@ impl AnyProtoClient {
         M: RequestMessage,
         E: 'static,
         H: 'static + Sync + Fn(Entity<E>, TypedEnvelope<M>, AsyncApp) -> F + Send + Sync,
-        F: 'static + Future<Output = anyhow::Result<M::Response>>,
+        F: 'static + Future<Output = Result<M::Response>>,
     {
-        self.0.message_handler_set().lock().add_message_handler(
-            TypeId::of::<M>(),
-            entity.into(),
-            Arc::new(move |entity, envelope, client, cx| {
-                let entity = entity.downcast::<E>().unwrap();
-                let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
-                let request_id = envelope.message_id();
-                handler(entity, *envelope, cx)
-                    .then(move |result| async move {
-                        match result {
-                            Ok(response) => {
-                                client.send_response(request_id, response)?;
-                                Ok(())
-                            }
-                            Err(error) => {
-                                client.send_response(request_id, error.to_proto())?;
-                                Err(error)
+        self.0
+            .client
+            .message_handler_set()
+            .lock()
+            .add_message_handler(
+                TypeId::of::<M>(),
+                entity.into(),
+                Arc::new(move |entity, envelope, client, cx| {
+                    let entity = entity.downcast::<E>().unwrap();
+                    let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+                    let request_id = envelope.message_id();
+                    handler(entity, *envelope, cx)
+                        .then(move |result| async move {
+                            match result {
+                                Ok(response) => {
+                                    client.send_response(request_id, response)?;
+                                    Ok(())
+                                }
+                                Err(error) => {
+                                    client.send_response(request_id, error.to_proto())?;
+                                    Err(error)
+                                }
                             }
-                        }
-                    })
-                    .boxed_local()
-            }),
-        )
+                        })
+                        .boxed_local()
+                }),
+            )
     }
 
     pub fn add_entity_request_handler<M, E, H, F>(&self, handler: H)
@@ -245,7 +414,7 @@ impl AnyProtoClient {
         M: EnvelopedMessage + RequestMessage + EntityMessage,
         E: 'static,
         H: 'static + Sync + Send + Fn(gpui::Entity<E>, TypedEnvelope<M>, AsyncApp) -> F,
-        F: 'static + Future<Output = anyhow::Result<M::Response>>,
+        F: 'static + Future<Output = Result<M::Response>>,
     {
         let message_type_id = TypeId::of::<M>();
         let entity_type_id = TypeId::of::<E>();
@@ -257,6 +426,7 @@ impl AnyProtoClient {
                 .remote_entity_id()
         };
         self.0
+            .client
             .message_handler_set()
             .lock()
             .add_entity_message_handler(
@@ -290,7 +460,7 @@ impl AnyProtoClient {
         M: EnvelopedMessage + EntityMessage,
         E: 'static,
         H: 'static + Sync + Send + Fn(gpui::Entity<E>, TypedEnvelope<M>, AsyncApp) -> F,
-        F: 'static + Future<Output = anyhow::Result<()>>,
+        F: 'static + Future<Output = Result<()>>,
     {
         let message_type_id = TypeId::of::<M>();
         let entity_type_id = TypeId::of::<E>();
@@ -302,6 +472,7 @@ impl AnyProtoClient {
                 .remote_entity_id()
         };
         self.0
+            .client
             .message_handler_set()
             .lock()
             .add_entity_message_handler(
@@ -319,7 +490,7 @@ impl AnyProtoClient {
     pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) {
         let id = (TypeId::of::<E>(), remote_id);
 
-        let mut message_handlers = self.0.message_handler_set().lock();
+        let mut message_handlers = self.0.client.message_handler_set().lock();
         if message_handlers
             .entities_by_type_and_remote_id
             .contains_key(&id)
@@ -335,3 +506,16 @@ impl AnyProtoClient {
         );
     }
 }
+
+fn to_any_envelope<T: EnvelopedMessage>(
+    envelope: &TypedEnvelope<proto::LspQueryResponse>,
+    response: T,
+) -> Box<dyn AnyTypedEnvelope> {
+    Box::new(proto::TypedEnvelope {
+        sender_id: envelope.sender_id,
+        original_sender_id: envelope.original_sender_id,
+        message_id: envelope.message_id,
+        received_at: envelope.received_at,
+        payload: response,
+    }) as Box<_>
+}