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 }
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}