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 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 } else {
181 Ok(())
182 }
183 })
184 .context("lsp ext run flycheck")?;
185 };
186 anyhow::Ok(())
187 })
188}
189
190pub fn clear_flycheck(
191 project: Entity<Project>,
192 buffer_path: Option<ProjectPath>,
193 cx: &mut App,
194) -> Task<anyhow::Result<()>> {
195 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
196 let lsp_store = project.read(cx).lsp_store();
197 let buffer = buffer_path.map(|buffer_path| {
198 project.update(cx, |project, cx| {
199 project.buffer_store().update(cx, |buffer_store, cx| {
200 buffer_store.open_buffer(buffer_path, cx)
201 })
202 })
203 });
204
205 cx.spawn(async move |cx| {
206 let buffer = match buffer {
207 Some(buffer) => Some(buffer.await?),
208 None => None,
209 };
210 let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx)
211 else {
212 return Ok(());
213 };
214
215 if let Some((client, project_id)) = upstream_client {
216 let request = proto::LspExtClearFlycheck {
217 project_id,
218 language_server_id: rust_analyzer_server.to_proto(),
219 };
220 client
221 .request(request)
222 .await
223 .context("lsp ext clear flycheck proto request")?;
224 } else {
225 lsp_store
226 .read_with(cx, |lsp_store, _| {
227 if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
228 server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(())
229 } else {
230 Ok(())
231 }
232 })
233 .context("lsp ext clear flycheck")?;
234 };
235 anyhow::Ok(())
236 })
237}
238
239fn find_rust_analyzer_server(
240 project: &Entity<Project>,
241 buffer: Option<&Entity<Buffer>>,
242 cx: &mut AsyncApp,
243) -> Option<LanguageServerId> {
244 project.read_with(cx, |project, cx| {
245 buffer
246 .and_then(|buffer| {
247 project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx)
248 })
249 // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup
250 // and use project's rust-analyzer if it's the only one.
251 .or_else(|| {
252 let rust_analyzer_servers = project
253 .lsp_store()
254 .read(cx)
255 .language_server_statuses
256 .iter()
257 .filter_map(|(server_id, server_status)| {
258 if server_status.name == RUST_ANALYZER_NAME {
259 Some(*server_id)
260 } else {
261 None
262 }
263 })
264 .collect::<Vec<_>>();
265 if rust_analyzer_servers.len() == 1 {
266 rust_analyzer_servers.first().copied()
267 } else {
268 None
269 }
270 })
271 })
272}