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