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