rust_analyzer_ext.rs

  1use ::serde::{Deserialize, Serialize};
  2use anyhow::Context as _;
  3use gpui::{App, Entity, PromptLevel, Task, WeakEntity};
  4use lsp::LanguageServer;
  5use rpc::proto;
  6
  7use crate::{
  8    LanguageServerPromptRequest, LspStore, LspStoreEvent, Project, ProjectPath, lsp_store,
  9};
 10
 11pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
 12pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc";
 13
 14/// Experimental: Informs the end user about the state of the server
 15///
 16/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status)
 17#[derive(Debug)]
 18enum ServerStatus {}
 19
 20/// Other(String) variant to handle unknown values due to this still being experimental
 21#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
 22#[serde(rename_all = "camelCase")]
 23enum ServerHealthStatus {
 24    Ok,
 25    Warning,
 26    Error,
 27    Other(String),
 28}
 29
 30#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
 31#[serde(rename_all = "camelCase")]
 32struct ServerStatusParams {
 33    pub health: ServerHealthStatus,
 34    pub message: Option<String>,
 35}
 36
 37impl lsp::notification::Notification for ServerStatus {
 38    type Params = ServerStatusParams;
 39    const METHOD: &'static str = "experimental/serverStatus";
 40}
 41
 42pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
 43    let name = language_server.name();
 44    let server_id = language_server.server_id();
 45
 46    language_server
 47        .on_notification::<ServerStatus, _>({
 48            let name = name.to_string();
 49            move |params, cx| {
 50                let name = name.to_string();
 51                if let Some(ref message) = params.message {
 52                    let message = message.trim();
 53                    if !message.is_empty() {
 54                        let formatted_message = format!(
 55                            "Language server {name} (id {server_id}) status update: {message}"
 56                        );
 57                        match params.health {
 58                            ServerHealthStatus::Ok => log::info!("{formatted_message}"),
 59                            ServerHealthStatus::Warning => log::warn!("{formatted_message}"),
 60                            ServerHealthStatus::Error => {
 61                                log::error!("{formatted_message}");
 62                                let (tx, _rx) = smol::channel::bounded(1);
 63                                let request = LanguageServerPromptRequest {
 64                                    level: PromptLevel::Critical,
 65                                    message: params.message.unwrap_or_default(),
 66                                    actions: Vec::new(),
 67                                    response_channel: tx,
 68                                    lsp_name: name.clone(),
 69                                };
 70                                lsp_store
 71                                    .update(cx, |_, cx| {
 72                                        cx.emit(LspStoreEvent::LanguageServerPrompt(request));
 73                                    })
 74                                    .ok();
 75                            }
 76                            ServerHealthStatus::Other(status) => {
 77                                log::info!("Unknown server health: {status}\n{formatted_message}")
 78                            }
 79                        }
 80                    }
 81                }
 82            }
 83        })
 84        .detach();
 85}
 86
 87pub fn cancel_flycheck(
 88    project: Entity<Project>,
 89    buffer_path: ProjectPath,
 90    cx: &mut App,
 91) -> Task<anyhow::Result<()>> {
 92    let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
 93    let lsp_store = project.read(cx).lsp_store();
 94    let buffer = project.update(cx, |project, cx| {
 95        project.buffer_store().update(cx, |buffer_store, cx| {
 96            buffer_store.open_buffer(buffer_path, cx)
 97        })
 98    });
 99
100    cx.spawn(async move |cx| {
101        let buffer = buffer.await?;
102        let Some(rust_analyzer_server) = project
103            .update(cx, |project, cx| {
104                buffer.update(cx, |buffer, cx| {
105                    project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx)
106                })
107            })?
108            .await
109        else {
110            return Ok(());
111        };
112        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?;
113
114        if let Some((client, project_id)) = upstream_client {
115            let request = proto::LspExtCancelFlycheck {
116                project_id,
117                buffer_id,
118                language_server_id: rust_analyzer_server.to_proto(),
119            };
120            client
121                .request(request)
122                .await
123                .context("lsp ext cancel flycheck proto request")?;
124        } else {
125            lsp_store
126                .read_with(cx, |lsp_store, _| {
127                    if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
128                        server.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())?;
129                    }
130                    anyhow::Ok(())
131                })?
132                .context("lsp ext cancel flycheck")?;
133        };
134        anyhow::Ok(())
135    })
136}
137
138pub fn run_flycheck(
139    project: Entity<Project>,
140    buffer_path: ProjectPath,
141    cx: &mut App,
142) -> Task<anyhow::Result<()>> {
143    let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
144    let lsp_store = project.read(cx).lsp_store();
145    let buffer = project.update(cx, |project, cx| {
146        project.buffer_store().update(cx, |buffer_store, cx| {
147            buffer_store.open_buffer(buffer_path, cx)
148        })
149    });
150
151    cx.spawn(async move |cx| {
152        let buffer = buffer.await?;
153        let Some(rust_analyzer_server) = project
154            .update(cx, |project, cx| {
155                buffer.update(cx, |buffer, cx| {
156                    project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx)
157                })
158            })?
159            .await
160        else {
161            return Ok(());
162        };
163        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?;
164
165        if let Some((client, project_id)) = upstream_client {
166            let request = proto::LspExtRunFlycheck {
167                project_id,
168                buffer_id,
169                language_server_id: rust_analyzer_server.to_proto(),
170                current_file_only: false,
171            };
172            client
173                .request(request)
174                .await
175                .context("lsp ext run flycheck proto request")?;
176        } else {
177            lsp_store
178                .read_with(cx, |lsp_store, _| {
179                    if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
180                        server.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
181                            &lsp_store::lsp_ext_command::RunFlycheckParams {
182                                text_document: None,
183                            },
184                        )?;
185                    }
186                    anyhow::Ok(())
187                })?
188                .context("lsp ext run flycheck")?;
189        };
190        anyhow::Ok(())
191    })
192}
193
194pub fn clear_flycheck(
195    project: Entity<Project>,
196    buffer_path: ProjectPath,
197    cx: &mut App,
198) -> Task<anyhow::Result<()>> {
199    let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
200    let lsp_store = project.read(cx).lsp_store();
201    let buffer = project.update(cx, |project, cx| {
202        project.buffer_store().update(cx, |buffer_store, cx| {
203            buffer_store.open_buffer(buffer_path, cx)
204        })
205    });
206
207    cx.spawn(async move |cx| {
208        let buffer = buffer.await?;
209        let Some(rust_analyzer_server) = project
210            .update(cx, |project, cx| {
211                buffer.update(cx, |buffer, cx| {
212                    project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx)
213                })
214            })?
215            .await
216        else {
217            return Ok(());
218        };
219        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?;
220
221        if let Some((client, project_id)) = upstream_client {
222            let request = proto::LspExtClearFlycheck {
223                project_id,
224                buffer_id,
225                language_server_id: rust_analyzer_server.to_proto(),
226            };
227            client
228                .request(request)
229                .await
230                .context("lsp ext clear flycheck proto request")?;
231        } else {
232            lsp_store
233                .read_with(cx, |lsp_store, _| {
234                    if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
235                        server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())?;
236                    }
237                    anyhow::Ok(())
238                })?
239                .context("lsp ext clear flycheck")?;
240        };
241        anyhow::Ok(())
242    })
243}