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 ¶ms.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 } else {
124 Ok(())
125 }
126 })
127 .context("lsp ext cancel flycheck")??;
128 };
129 anyhow::Ok(())
130 })
131}
132
133pub fn run_flycheck(
134 project: Entity<Project>,
135 buffer_path: Option<ProjectPath>,
136 cx: &mut App,
137) -> Task<anyhow::Result<()>> {
138 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
139 let lsp_store = project.read(cx).lsp_store();
140 let buffer = buffer_path.map(|buffer_path| {
141 project.update(cx, |project, cx| {
142 project.buffer_store().update(cx, |buffer_store, cx| {
143 buffer_store.open_buffer(buffer_path, cx)
144 })
145 })
146 });
147
148 cx.spawn(async move |cx| {
149 let buffer = match buffer {
150 Some(buffer) => Some(buffer.await?),
151 None => None,
152 };
153 let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
154 else {
155 return Ok(());
156 };
157
158 if let Some((client, project_id)) = upstream_client {
159 let buffer_id = buffer
160 .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto()))
161 .transpose()?;
162 let request = proto::LspExtRunFlycheck {
163 project_id,
164 buffer_id,
165 language_server_id: rust_analyzer_server.to_proto(),
166 current_file_only: false,
167 };
168 client
169 .request(request)
170 .await
171 .context("lsp ext run flycheck proto request")?;
172 } else {
173 lsp_store
174 .read_with(cx, |lsp_store, _| {
175 if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
176 server.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
177 lsp_store::lsp_ext_command::RunFlycheckParams {
178 text_document: None,
179 },
180 )
181 } else {
182 Ok(())
183 }
184 })
185 .context("lsp ext run flycheck")??;
186 };
187 anyhow::Ok(())
188 })
189}
190
191pub fn clear_flycheck(
192 project: Entity<Project>,
193 buffer_path: Option<ProjectPath>,
194 cx: &mut App,
195) -> Task<anyhow::Result<()>> {
196 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
197 let lsp_store = project.read(cx).lsp_store();
198 let buffer = buffer_path.map(|buffer_path| {
199 project.update(cx, |project, cx| {
200 project.buffer_store().update(cx, |buffer_store, cx| {
201 buffer_store.open_buffer(buffer_path, cx)
202 })
203 })
204 });
205
206 cx.spawn(async move |cx| {
207 let buffer = match buffer {
208 Some(buffer) => Some(buffer.await?),
209 None => None,
210 };
211 let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
212 else {
213 return Ok(());
214 };
215
216 if let Some((client, project_id)) = upstream_client {
217 let request = proto::LspExtClearFlycheck {
218 project_id,
219 language_server_id: rust_analyzer_server.to_proto(),
220 };
221 client
222 .request(request)
223 .await
224 .context("lsp ext clear flycheck proto request")?;
225 } else {
226 lsp_store
227 .read_with(cx, |lsp_store, _| {
228 if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
229 server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(())
230 } else {
231 Ok(())
232 }
233 })
234 .context("lsp ext clear flycheck")??;
235 };
236 anyhow::Ok(())
237 })
238}
239
240fn find_rust_analyzer_server(
241 project: &Entity<Project>,
242 buffer: Option<&Entity<Buffer>>,
243 cx: &mut AsyncApp,
244) -> Option<LanguageServerId> {
245 project
246 .read_with(cx, |project, cx| {
247 buffer
248 .and_then(|buffer| {
249 project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
250 })
251 // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup
252 // and use project's rust-analyzer if it's the only one.
253 .or_else(|| {
254 let rust_analyzer_servers = project
255 .lsp_store()
256 .read(cx)
257 .language_server_statuses
258 .iter()
259 .filter_map(|(server_id, server_status)| {
260 if server_status.name == RUST_ANALYZER_NAME {
261 Some(*server_id)
262 } else {
263 None
264 }
265 })
266 .collect::<Vec<_>>();
267 if rust_analyzer_servers.len() == 1 {
268 rust_analyzer_servers.first().copied()
269 } else {
270 None
271 }
272 })
273 })
274 .ok()?
275}