Be more lenient when dealing with rust-analyzer's flycheck commands (#36782)

Kirill Bulatov created

Flycheck commands are global and makes sense to fall back to looking up
project's rust-analyzer even if the commands are run on a non-rust
buffer. If multiple rust-analyzers are found in the project, avoid
ambiguous commands and bail (as before).

Closes #ISSUE

Release Notes:

- Made it possible to run rust-analyzer's flycheck actions from anywhere
in the project

Change summary

crates/diagnostics/src/diagnostics.rs             |   4 
crates/editor/src/rust_analyzer_ext.rs            |  35 ++--
crates/project/src/lsp_store.rs                   |  23 ++-
crates/project/src/lsp_store/rust_analyzer_ext.rs | 108 +++++++++++-----
crates/proto/proto/lsp.proto                      |   8 
5 files changed, 114 insertions(+), 64 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -438,7 +438,7 @@ impl ProjectDiagnosticsEditor {
             for buffer_path in diagnostics_sources.iter().cloned() {
                 if cx
                     .update(|cx| {
-                        fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
+                        fetch_tasks.push(run_flycheck(project.clone(), Some(buffer_path), cx));
                     })
                     .is_err()
                 {
@@ -462,7 +462,7 @@ impl ProjectDiagnosticsEditor {
             .iter()
             .cloned()
         {
-            cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
+            cancel_gasks.push(cancel_flycheck(self.project.clone(), Some(buffer_path), cx));
         }
 
         self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {

crates/editor/src/rust_analyzer_ext.rs 🔗

@@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool {
 }
 
 pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
+    if editor.read(cx).project().is_some_and(|project| {
+        project
+            .read(cx)
+            .language_server_statuses(cx)
+            .any(|(_, status)| status.name == RUST_ANALYZER_NAME)
+    }) {
+        register_action(editor, window, cancel_flycheck_action);
+        register_action(editor, window, run_flycheck_action);
+        register_action(editor, window, clear_flycheck_action);
+    }
+
     if editor
         .read(cx)
         .buffer()
@@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
         register_action(editor, window, go_to_parent_module);
         register_action(editor, window, expand_macro_recursively);
         register_action(editor, window, open_docs);
-        register_action(editor, window, cancel_flycheck_action);
-        register_action(editor, window, run_flycheck_action);
-        register_action(editor, window, clear_flycheck_action);
     }
 }
 
@@ -309,7 +317,7 @@ fn cancel_flycheck_action(
     let Some(project) = &editor.project else {
         return;
     };
-    let Some(buffer_id) = editor
+    let buffer_id = editor
         .selections
         .disjoint_anchors()
         .iter()
@@ -321,10 +329,7 @@ fn cancel_flycheck_action(
                 .read(cx)
                 .entry_id(cx)?;
             project.path_for_entry(entry_id, cx)
-        })
-    else {
-        return;
-    };
+        });
     cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
 }
 
@@ -337,7 +342,7 @@ fn run_flycheck_action(
     let Some(project) = &editor.project else {
         return;
     };
-    let Some(buffer_id) = editor
+    let buffer_id = editor
         .selections
         .disjoint_anchors()
         .iter()
@@ -349,10 +354,7 @@ fn run_flycheck_action(
                 .read(cx)
                 .entry_id(cx)?;
             project.path_for_entry(entry_id, cx)
-        })
-    else {
-        return;
-    };
+        });
     run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
 }
 
@@ -365,7 +367,7 @@ fn clear_flycheck_action(
     let Some(project) = &editor.project else {
         return;
     };
-    let Some(buffer_id) = editor
+    let buffer_id = editor
         .selections
         .disjoint_anchors()
         .iter()
@@ -377,9 +379,6 @@ fn clear_flycheck_action(
                 .read(cx)
                 .entry_id(cx)?;
             project.path_for_entry(entry_id, cx)
-        })
-    else {
-        return;
-    };
+        });
     clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
 }

crates/project/src/lsp_store.rs 🔗

@@ -9029,13 +9029,22 @@ impl LspStore {
         lsp_store.update(&mut cx, |lsp_store, cx| {
             if let Some(server) = lsp_store.language_server_for_id(server_id) {
                 let text_document = if envelope.payload.current_file_only {
-                    let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
-                    lsp_store
-                        .buffer_store()
-                        .read(cx)
-                        .get(buffer_id)
-                        .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)))
-                        .map(|path| make_text_document_identifier(&path))
+                    let buffer_id = envelope
+                        .payload
+                        .buffer_id
+                        .map(|id| BufferId::new(id))
+                        .transpose()?;
+                    buffer_id
+                        .and_then(|buffer_id| {
+                            lsp_store
+                                .buffer_store()
+                                .read(cx)
+                                .get(buffer_id)
+                                .and_then(|buffer| {
+                                    Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))
+                                })
+                                .map(|path| make_text_document_identifier(&path))
+                        })
                         .transpose()?
                 } else {
                     None

crates/project/src/lsp_store/rust_analyzer_ext.rs 🔗

@@ -1,8 +1,8 @@
 use ::serde::{Deserialize, Serialize};
 use anyhow::Context as _;
-use gpui::{App, Entity, Task, WeakEntity};
-use language::ServerHealth;
-use lsp::{LanguageServer, LanguageServerName};
+use gpui::{App, AsyncApp, Entity, Task, WeakEntity};
+use language::{Buffer, ServerHealth};
+use lsp::{LanguageServer, LanguageServerId, LanguageServerName};
 use rpc::proto;
 
 use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store};
@@ -83,31 +83,32 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
 
 pub fn cancel_flycheck(
     project: Entity<Project>,
-    buffer_path: ProjectPath,
+    buffer_path: Option<ProjectPath>,
     cx: &mut App,
 ) -> Task<anyhow::Result<()>> {
     let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
     let lsp_store = project.read(cx).lsp_store();
-    let buffer = project.update(cx, |project, cx| {
-        project.buffer_store().update(cx, |buffer_store, cx| {
-            buffer_store.open_buffer(buffer_path, cx)
+    let buffer = buffer_path.map(|buffer_path| {
+        project.update(cx, |project, cx| {
+            project.buffer_store().update(cx, |buffer_store, cx| {
+                buffer_store.open_buffer(buffer_path, cx)
+            })
         })
     });
 
     cx.spawn(async move |cx| {
-        let buffer = buffer.await?;
-        let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| {
-            project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
-        })?
+        let buffer = match buffer {
+            Some(buffer) => Some(buffer.await?),
+            None => None,
+        };
+        let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
         else {
             return Ok(());
         };
-        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?;
 
         if let Some((client, project_id)) = upstream_client {
             let request = proto::LspExtCancelFlycheck {
                 project_id,
-                buffer_id,
                 language_server_id: rust_analyzer_server.to_proto(),
             };
             client
@@ -130,28 +131,33 @@ pub fn cancel_flycheck(
 
 pub fn run_flycheck(
     project: Entity<Project>,
-    buffer_path: ProjectPath,
+    buffer_path: Option<ProjectPath>,
     cx: &mut App,
 ) -> Task<anyhow::Result<()>> {
     let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
     let lsp_store = project.read(cx).lsp_store();
-    let buffer = project.update(cx, |project, cx| {
-        project.buffer_store().update(cx, |buffer_store, cx| {
-            buffer_store.open_buffer(buffer_path, cx)
+    let buffer = buffer_path.map(|buffer_path| {
+        project.update(cx, |project, cx| {
+            project.buffer_store().update(cx, |buffer_store, cx| {
+                buffer_store.open_buffer(buffer_path, cx)
+            })
         })
     });
 
     cx.spawn(async move |cx| {
-        let buffer = buffer.await?;
-        let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| {
-            project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
-        })?
+        let buffer = match buffer {
+            Some(buffer) => Some(buffer.await?),
+            None => None,
+        };
+        let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
         else {
             return Ok(());
         };
-        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?;
 
         if let Some((client, project_id)) = upstream_client {
+            let buffer_id = buffer
+                .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto()))
+                .transpose()?;
             let request = proto::LspExtRunFlycheck {
                 project_id,
                 buffer_id,
@@ -182,31 +188,32 @@ pub fn run_flycheck(
 
 pub fn clear_flycheck(
     project: Entity<Project>,
-    buffer_path: ProjectPath,
+    buffer_path: Option<ProjectPath>,
     cx: &mut App,
 ) -> Task<anyhow::Result<()>> {
     let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
     let lsp_store = project.read(cx).lsp_store();
-    let buffer = project.update(cx, |project, cx| {
-        project.buffer_store().update(cx, |buffer_store, cx| {
-            buffer_store.open_buffer(buffer_path, cx)
+    let buffer = buffer_path.map(|buffer_path| {
+        project.update(cx, |project, cx| {
+            project.buffer_store().update(cx, |buffer_store, cx| {
+                buffer_store.open_buffer(buffer_path, cx)
+            })
         })
     });
 
     cx.spawn(async move |cx| {
-        let buffer = buffer.await?;
-        let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| {
-            project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
-        })?
+        let buffer = match buffer {
+            Some(buffer) => Some(buffer.await?),
+            None => None,
+        };
+        let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
         else {
             return Ok(());
         };
-        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?;
 
         if let Some((client, project_id)) = upstream_client {
             let request = proto::LspExtClearFlycheck {
                 project_id,
-                buffer_id,
                 language_server_id: rust_analyzer_server.to_proto(),
             };
             client
@@ -226,3 +233,40 @@ pub fn clear_flycheck(
         anyhow::Ok(())
     })
 }
+
+fn find_rust_analyzer_server(
+    project: &Entity<Project>,
+    buffer: Option<&Entity<Buffer>>,
+    cx: &mut AsyncApp,
+) -> Option<LanguageServerId> {
+    project
+        .read_with(cx, |project, cx| {
+            buffer
+                .and_then(|buffer| {
+                    project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
+                })
+                // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup
+                // and use project's rust-analyzer if it's the only one.
+                .or_else(|| {
+                    let rust_analyzer_servers = project
+                        .lsp_store()
+                        .read(cx)
+                        .language_server_statuses
+                        .iter()
+                        .filter_map(|(server_id, server_status)| {
+                            if server_status.name == RUST_ANALYZER_NAME {
+                                Some(*server_id)
+                            } else {
+                                None
+                            }
+                        })
+                        .collect::<Vec<_>>();
+                    if rust_analyzer_servers.len() == 1 {
+                        rust_analyzer_servers.first().copied()
+                    } else {
+                        None
+                    }
+                })
+        })
+        .ok()?
+}

crates/proto/proto/lsp.proto 🔗

@@ -834,21 +834,19 @@ message LspRunnable {
 
 message LspExtCancelFlycheck {
     uint64 project_id = 1;
-    uint64 buffer_id = 2;
-    uint64 language_server_id = 3;
+    uint64 language_server_id = 2;
 }
 
 message LspExtRunFlycheck {
     uint64 project_id = 1;
-    uint64 buffer_id = 2;
+    optional uint64 buffer_id = 2;
     uint64 language_server_id = 3;
     bool current_file_only = 4;
 }
 
 message LspExtClearFlycheck {
     uint64 project_id = 1;
-    uint64 buffer_id = 2;
-    uint64 language_server_id = 3;
+    uint64 language_server_id = 2;
 }
 
 message LspDiagnosticRelatedInformation {