rust_analyzer_ext.rs

  1use ::serde::{Deserialize, Serialize};
  2use anyhow::Context as _;
  3use gpui::{App, AsyncApp, Entity, Task, WeakEntity};
  4use language::{Buffer, ServerHealth};
  5use lsp::{LanguageServer, LanguageServerId, LanguageServerName};
  6use rpc::proto;
  7
  8use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store};
  9
 10pub const RUST_ANALYZER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
 11pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc";
 12
 13/// Experimental: Informs the end user about the state of the server
 14///
 15/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status)
 16#[derive(Debug)]
 17enum ServerStatus {}
 18
 19#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
 20#[serde(rename_all = "camelCase")]
 21struct ServerStatusParams {
 22    pub health: ServerHealth,
 23    pub message: Option<String>,
 24}
 25
 26impl lsp::notification::Notification for ServerStatus {
 27    type Params = ServerStatusParams;
 28    const METHOD: &'static str = "experimental/serverStatus";
 29}
 30
 31pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
 32    let name = language_server.name();
 33    let server_id = language_server.server_id();
 34
 35    language_server
 36        .on_notification::<ServerStatus, _>({
 37            move |params, cx| {
 38                let message = params.message;
 39                let log_message = message.as_ref().map(|message| {
 40                    format!("Language server {name} (id {server_id}) status update: {message}")
 41                });
 42                let status = match &params.health {
 43                    ServerHealth::Ok => {
 44                        if let Some(log_message) = log_message {
 45                            log::info!("{log_message}");
 46                        }
 47                        proto::ServerHealth::Ok
 48                    }
 49                    ServerHealth::Warning => {
 50                        if let Some(log_message) = log_message {
 51                            log::warn!("{log_message}");
 52                        }
 53                        proto::ServerHealth::Warning
 54                    }
 55                    ServerHealth::Error => {
 56                        if let Some(log_message) = log_message {
 57                            log::error!("{log_message}");
 58                        }
 59                        proto::ServerHealth::Error
 60                    }
 61                };
 62
 63                lsp_store
 64                    .update(cx, |_, cx| {
 65                        cx.emit(LspStoreEvent::LanguageServerUpdate {
 66                            language_server_id: server_id,
 67                            name: Some(name.clone()),
 68                            message: proto::update_language_server::Variant::StatusUpdate(
 69                                proto::StatusUpdate {
 70                                    message,
 71                                    status: Some(proto::status_update::Status::Health(
 72                                        status as i32,
 73                                    )),
 74                                },
 75                            ),
 76                        });
 77                    })
 78                    .ok();
 79            }
 80        })
 81        .detach();
 82}
 83
 84pub fn cancel_flycheck(
 85    project: Entity<Project>,
 86    buffer_path: Option<ProjectPath>,
 87    cx: &mut App,
 88) -> Task<anyhow::Result<()>> {
 89    let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
 90    let lsp_store = project.read(cx).lsp_store();
 91    let buffer = buffer_path.map(|buffer_path| {
 92        project.update(cx, |project, cx| {
 93            project.buffer_store().update(cx, |buffer_store, cx| {
 94                buffer_store.open_buffer(buffer_path, cx)
 95            })
 96        })
 97    });
 98
 99    cx.spawn(async move |cx| {
100        let buffer = match buffer {
101            Some(buffer) => Some(buffer.await?),
102            None => None,
103        };
104        let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
105        else {
106            return Ok(());
107        };
108
109        if let Some((client, project_id)) = upstream_client {
110            let request = proto::LspExtCancelFlycheck {
111                project_id,
112                language_server_id: rust_analyzer_server.to_proto(),
113            };
114            client
115                .request(request)
116                .await
117                .context("lsp ext cancel flycheck proto request")?;
118        } else {
119            lsp_store
120                .read_with(cx, |lsp_store, _| {
121                    if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
122                        server.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())?;
123                    }
124                    anyhow::Ok(())
125                })?
126                .context("lsp ext cancel flycheck")?;
127        };
128        anyhow::Ok(())
129    })
130}
131
132pub fn run_flycheck(
133    project: Entity<Project>,
134    buffer_path: Option<ProjectPath>,
135    cx: &mut App,
136) -> Task<anyhow::Result<()>> {
137    let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
138    let lsp_store = project.read(cx).lsp_store();
139    let buffer = buffer_path.map(|buffer_path| {
140        project.update(cx, |project, cx| {
141            project.buffer_store().update(cx, |buffer_store, cx| {
142                buffer_store.open_buffer(buffer_path, cx)
143            })
144        })
145    });
146
147    cx.spawn(async move |cx| {
148        let buffer = match buffer {
149            Some(buffer) => Some(buffer.await?),
150            None => None,
151        };
152        let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
153        else {
154            return Ok(());
155        };
156
157        if let Some((client, project_id)) = upstream_client {
158            let buffer_id = buffer
159                .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto()))
160                .transpose()?;
161            let request = proto::LspExtRunFlycheck {
162                project_id,
163                buffer_id,
164                language_server_id: rust_analyzer_server.to_proto(),
165                current_file_only: false,
166            };
167            client
168                .request(request)
169                .await
170                .context("lsp ext run flycheck proto request")?;
171        } else {
172            lsp_store
173                .read_with(cx, |lsp_store, _| {
174                    if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
175                        server.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
176                            &lsp_store::lsp_ext_command::RunFlycheckParams {
177                                text_document: None,
178                            },
179                        )?;
180                    }
181                    anyhow::Ok(())
182                })?
183                .context("lsp ext run flycheck")?;
184        };
185        anyhow::Ok(())
186    })
187}
188
189pub fn clear_flycheck(
190    project: Entity<Project>,
191    buffer_path: Option<ProjectPath>,
192    cx: &mut App,
193) -> Task<anyhow::Result<()>> {
194    let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
195    let lsp_store = project.read(cx).lsp_store();
196    let buffer = buffer_path.map(|buffer_path| {
197        project.update(cx, |project, cx| {
198            project.buffer_store().update(cx, |buffer_store, cx| {
199                buffer_store.open_buffer(buffer_path, cx)
200            })
201        })
202    });
203
204    cx.spawn(async move |cx| {
205        let buffer = match buffer {
206            Some(buffer) => Some(buffer.await?),
207            None => None,
208        };
209        let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
210        else {
211            return Ok(());
212        };
213
214        if let Some((client, project_id)) = upstream_client {
215            let request = proto::LspExtClearFlycheck {
216                project_id,
217                language_server_id: rust_analyzer_server.to_proto(),
218            };
219            client
220                .request(request)
221                .await
222                .context("lsp ext clear flycheck proto request")?;
223        } else {
224            lsp_store
225                .read_with(cx, |lsp_store, _| {
226                    if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
227                        server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())?;
228                    }
229                    anyhow::Ok(())
230                })?
231                .context("lsp ext clear flycheck")?;
232        };
233        anyhow::Ok(())
234    })
235}
236
237fn find_rust_analyzer_server(
238    project: &Entity<Project>,
239    buffer: Option<&Entity<Buffer>>,
240    cx: &mut AsyncApp,
241) -> Option<LanguageServerId> {
242    project
243        .read_with(cx, |project, cx| {
244            buffer
245                .and_then(|buffer| {
246                    project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
247                })
248                // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup
249                // and use project's rust-analyzer if it's the only one.
250                .or_else(|| {
251                    let rust_analyzer_servers = project
252                        .lsp_store()
253                        .read(cx)
254                        .language_server_statuses
255                        .iter()
256                        .filter_map(|(server_id, server_status)| {
257                            if server_status.name == RUST_ANALYZER_NAME {
258                                Some(*server_id)
259                            } else {
260                                None
261                            }
262                        })
263                        .collect::<Vec<_>>();
264                    if rust_analyzer_servers.len() == 1 {
265                        rust_analyzer_servers.first().copied()
266                    } else {
267                        None
268                    }
269                })
270        })
271        .ok()?
272}