1use std::{fs, path::Path};
2
3use anyhow::Context as _;
4use gpui::{App, AppContext as _, Context, Entity, Window};
5use language::{Capability, Language, proto::serialize_anchor};
6use multi_buffer::MultiBuffer;
7use project::{
8 ProjectItem,
9 lsp_command::location_link_from_proto,
10 lsp_store::{
11 lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
12 rust_analyzer_ext::{RUST_ANALYZER_NAME, cancel_flycheck, clear_flycheck, run_flycheck},
13 },
14};
15use rpc::proto;
16use text::ToPointUtf16;
17
18use crate::{
19 CancelFlycheck, ClearFlycheck, Editor, ExpandMacroRecursively, GoToParentModule,
20 GotoDefinitionKind, OpenDocs, RunFlycheck, element::register_action, hover_links::HoverLink,
21 lsp_ext::find_specific_language_server_in_selection,
22};
23
24fn is_rust_language(language: &Language) -> bool {
25 language.name() == "Rust".into()
26}
27
28pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
29 if editor.read(cx).project().is_some_and(|project| {
30 project
31 .read(cx)
32 .language_server_statuses(cx)
33 .any(|(_, status)| status.name == RUST_ANALYZER_NAME)
34 }) {
35 register_action(editor, window, cancel_flycheck_action);
36 register_action(editor, window, run_flycheck_action);
37 register_action(editor, window, clear_flycheck_action);
38 }
39
40 if editor
41 .read(cx)
42 .buffer()
43 .read(cx)
44 .all_buffers()
45 .into_iter()
46 .filter_map(|buffer| buffer.read(cx).language())
47 .any(|language| is_rust_language(language))
48 {
49 register_action(editor, window, go_to_parent_module);
50 register_action(editor, window, expand_macro_recursively);
51 register_action(editor, window, open_docs);
52 }
53}
54
55pub fn go_to_parent_module(
56 editor: &mut Editor,
57 _: &GoToParentModule,
58 window: &mut Window,
59 cx: &mut Context<Editor>,
60) {
61 if editor.selections.count() == 0 {
62 return;
63 }
64 let Some(project) = &editor.project else {
65 return;
66 };
67
68 let Some((trigger_anchor, _, server_to_query, buffer)) =
69 find_specific_language_server_in_selection(
70 editor,
71 cx,
72 is_rust_language,
73 RUST_ANALYZER_NAME,
74 )
75 else {
76 return;
77 };
78
79 let project = project.clone();
80 let lsp_store = project.read(cx).lsp_store();
81 let upstream_client = lsp_store.read(cx).upstream_client();
82 cx.spawn_in(window, async move |editor, cx| {
83 let location_links = if let Some((client, project_id)) = upstream_client {
84 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
85
86 let request = proto::LspExtGoToParentModule {
87 project_id,
88 buffer_id: buffer_id.to_proto(),
89 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
90 };
91 let response = client
92 .request(request)
93 .await
94 .context("lsp ext go to parent module proto request")?;
95 futures::future::join_all(
96 response
97 .links
98 .into_iter()
99 .map(|link| location_link_from_proto(link, lsp_store.clone(), cx)),
100 )
101 .await
102 .into_iter()
103 .collect::<anyhow::Result<_>>()
104 .context("go to parent module via collab")?
105 } else {
106 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
107 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
108 project
109 .update(cx, |project, cx| {
110 project.request_lsp(
111 buffer,
112 project::LanguageServerToQuery::Other(server_to_query),
113 project::lsp_store::lsp_ext_command::GoToParentModule { position },
114 cx,
115 )
116 })
117 .await
118 .context("go to parent module")?
119 };
120
121 editor
122 .update_in(cx, |editor, window, cx| {
123 editor.navigate_to_hover_links(
124 Some(GotoDefinitionKind::Declaration),
125 location_links.into_iter().map(HoverLink::Text).collect(),
126 false,
127 window,
128 cx,
129 )
130 })?
131 .await?;
132 anyhow::Ok(())
133 })
134 .detach_and_log_err(cx);
135}
136
137pub fn expand_macro_recursively(
138 editor: &mut Editor,
139 _: &ExpandMacroRecursively,
140 window: &mut Window,
141 cx: &mut Context<Editor>,
142) {
143 let Some(project) = &editor.project else {
144 return;
145 };
146 let Some(workspace) = editor.workspace() else {
147 return;
148 };
149
150 let Some((trigger_anchor, rust_language, server_to_query, buffer)) =
151 find_specific_language_server_in_selection(
152 editor,
153 cx,
154 is_rust_language,
155 RUST_ANALYZER_NAME,
156 )
157 else {
158 return;
159 };
160 let project = project.clone();
161 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
162 cx.spawn_in(window, async move |_editor, cx| {
163 let macro_expansion = if let Some((client, project_id)) = upstream_client {
164 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id());
165 let request = proto::LspExtExpandMacro {
166 project_id,
167 buffer_id: buffer_id.to_proto(),
168 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
169 };
170 let response = client
171 .request(request)
172 .await
173 .context("lsp ext expand macro proto request")?;
174 ExpandedMacro {
175 name: response.name,
176 expansion: response.expansion,
177 }
178 } else {
179 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
180 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
181 project
182 .update(cx, |project, cx| {
183 project.request_lsp(
184 buffer,
185 project::LanguageServerToQuery::Other(server_to_query),
186 ExpandMacro { position },
187 cx,
188 )
189 })
190 .await
191 .context("expand macro")?
192 };
193
194 if macro_expansion.is_empty() {
195 log::info!(
196 "Empty macro expansion for position {:?}",
197 trigger_anchor.text_anchor
198 );
199 return Ok(());
200 }
201
202 let buffer = project
203 .update(cx, |project, cx| {
204 project.create_buffer(Some(rust_language), false, cx)
205 })
206 .await?;
207 workspace.update_in(cx, |workspace, window, cx| {
208 buffer.update(cx, |buffer, cx| {
209 buffer.set_text(macro_expansion.expansion, cx);
210 buffer.set_capability(Capability::ReadOnly, cx);
211 });
212 let multibuffer =
213 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
214 workspace.add_item_to_active_pane(
215 Box::new(cx.new(|cx| {
216 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
217 editor.set_read_only(true);
218 editor
219 })),
220 None,
221 true,
222 window,
223 cx,
224 );
225 })
226 })
227 .detach_and_log_err(cx);
228}
229
230pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mut Context<Editor>) {
231 if editor.selections.count() == 0 {
232 return;
233 }
234 let Some(project) = &editor.project else {
235 return;
236 };
237 let Some(workspace) = editor.workspace() else {
238 return;
239 };
240
241 let Some((trigger_anchor, _, server_to_query, buffer)) =
242 find_specific_language_server_in_selection(
243 editor,
244 cx,
245 is_rust_language,
246 RUST_ANALYZER_NAME,
247 )
248 else {
249 return;
250 };
251
252 let project = project.clone();
253 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
254 cx.spawn_in(window, async move |_editor, cx| {
255 let docs_urls = if let Some((client, project_id)) = upstream_client {
256 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
257 let request = proto::LspExtOpenDocs {
258 project_id,
259 buffer_id: buffer_id.to_proto(),
260 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
261 };
262 let response = client
263 .request(request)
264 .await
265 .context("lsp ext open docs proto request")?;
266 DocsUrls {
267 web: response.web,
268 local: response.local,
269 }
270 } else {
271 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
272 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
273 project
274 .update(cx, |project, cx| {
275 project.request_lsp(
276 buffer,
277 project::LanguageServerToQuery::Other(server_to_query),
278 project::lsp_store::lsp_ext_command::OpenDocs { position },
279 cx,
280 )
281 })
282 .await
283 .context("open docs")?
284 };
285
286 if docs_urls.is_empty() {
287 log::debug!(
288 "Empty docs urls for position {:?}",
289 trigger_anchor.text_anchor
290 );
291 return Ok(());
292 }
293
294 workspace.update(cx, |_workspace, cx| {
295 // Check if the local document exists, otherwise fallback to the online document.
296 // Open with the default browser.
297 if let Some(local_url) = docs_urls.local
298 && fs::metadata(Path::new(&local_url[8..])).is_ok()
299 {
300 cx.open_url(&local_url);
301 return;
302 }
303
304 if let Some(web_url) = docs_urls.web {
305 cx.open_url(&web_url);
306 }
307 });
308 anyhow::Ok(())
309 })
310 .detach_and_log_err(cx);
311}
312
313fn cancel_flycheck_action(
314 editor: &mut Editor,
315 _: &CancelFlycheck,
316 _: &mut Window,
317 cx: &mut Context<Editor>,
318) {
319 let Some(project) = &editor.project else {
320 return;
321 };
322 let buffer_id = editor
323 .selections
324 .disjoint_anchors_arc()
325 .iter()
326 .find_map(|selection| {
327 let buffer_id = selection
328 .start
329 .text_anchor
330 .buffer_id
331 .or(selection.end.text_anchor.buffer_id)?;
332 let project = project.read(cx);
333 let entry_id = project
334 .buffer_for_id(buffer_id, cx)?
335 .read(cx)
336 .entry_id(cx)?;
337 project.path_for_entry(entry_id, cx)
338 });
339 cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
340}
341
342fn run_flycheck_action(
343 editor: &mut Editor,
344 _: &RunFlycheck,
345 _: &mut Window,
346 cx: &mut Context<Editor>,
347) {
348 let Some(project) = &editor.project else {
349 return;
350 };
351 let buffer_id = editor
352 .selections
353 .disjoint_anchors_arc()
354 .iter()
355 .find_map(|selection| {
356 let buffer_id = selection
357 .start
358 .text_anchor
359 .buffer_id
360 .or(selection.end.text_anchor.buffer_id)?;
361 let project = project.read(cx);
362 let entry_id = project
363 .buffer_for_id(buffer_id, cx)?
364 .read(cx)
365 .entry_id(cx)?;
366 project.path_for_entry(entry_id, cx)
367 });
368 run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
369}
370
371fn clear_flycheck_action(
372 editor: &mut Editor,
373 _: &ClearFlycheck,
374 _: &mut Window,
375 cx: &mut Context<Editor>,
376) {
377 let Some(project) = &editor.project else {
378 return;
379 };
380 let buffer_id = editor
381 .selections
382 .disjoint_anchors_arc()
383 .iter()
384 .find_map(|selection| {
385 let buffer_id = selection
386 .start
387 .text_anchor
388 .buffer_id
389 .or(selection.end.text_anchor.buffer_id)?;
390 let project = project.read(cx);
391 let entry_id = project
392 .buffer_for_id(buffer_id, cx)?
393 .read(cx)
394 .entry_id(cx)?;
395 project.path_for_entry(entry_id, cx)
396 });
397 clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
398}