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| project.create_buffer(false, cx))?
204 .await?;
205 workspace.update_in(cx, |workspace, window, cx| {
206 buffer.update(cx, |buffer, cx| {
207 buffer.set_text(macro_expansion.expansion, cx);
208 buffer.set_language(Some(rust_language), cx);
209 buffer.set_capability(Capability::ReadOnly, cx);
210 });
211 let multibuffer =
212 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
213 workspace.add_item_to_active_pane(
214 Box::new(cx.new(|cx| {
215 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
216 editor.set_read_only(true);
217 editor
218 })),
219 None,
220 true,
221 window,
222 cx,
223 );
224 })
225 })
226 .detach_and_log_err(cx);
227}
228
229pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mut Context<Editor>) {
230 if editor.selections.count() == 0 {
231 return;
232 }
233 let Some(project) = &editor.project else {
234 return;
235 };
236 let Some(workspace) = editor.workspace() else {
237 return;
238 };
239
240 let Some((trigger_anchor, _, server_to_query, buffer)) =
241 find_specific_language_server_in_selection(
242 editor,
243 cx,
244 is_rust_language,
245 RUST_ANALYZER_NAME,
246 )
247 else {
248 return;
249 };
250
251 let project = project.clone();
252 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
253 cx.spawn_in(window, async move |_editor, cx| {
254 let docs_urls = if let Some((client, project_id)) = upstream_client {
255 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?;
256 let request = proto::LspExtOpenDocs {
257 project_id,
258 buffer_id: buffer_id.to_proto(),
259 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
260 };
261 let response = client
262 .request(request)
263 .await
264 .context("lsp ext open docs proto request")?;
265 DocsUrls {
266 web: response.web,
267 local: response.local,
268 }
269 } else {
270 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
271 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
272 project
273 .update(cx, |project, cx| {
274 project.request_lsp(
275 buffer,
276 project::LanguageServerToQuery::Other(server_to_query),
277 project::lsp_store::lsp_ext_command::OpenDocs { position },
278 cx,
279 )
280 })?
281 .await
282 .context("open docs")?
283 };
284
285 if docs_urls.is_empty() {
286 log::debug!(
287 "Empty docs urls for position {:?}",
288 trigger_anchor.text_anchor
289 );
290 return Ok(());
291 }
292
293 workspace.update(cx, |_workspace, cx| {
294 // Check if the local document exists, otherwise fallback to the online document.
295 // Open with the default browser.
296 if let Some(local_url) = docs_urls.local
297 && fs::metadata(Path::new(&local_url[8..])).is_ok()
298 {
299 cx.open_url(&local_url);
300 return;
301 }
302
303 if let Some(web_url) = docs_urls.web {
304 cx.open_url(&web_url);
305 }
306 })
307 })
308 .detach_and_log_err(cx);
309}
310
311fn cancel_flycheck_action(
312 editor: &mut Editor,
313 _: &CancelFlycheck,
314 _: &mut Window,
315 cx: &mut Context<Editor>,
316) {
317 let Some(project) = &editor.project else {
318 return;
319 };
320 let buffer_id = editor
321 .selections
322 .disjoint_anchors_arc()
323 .iter()
324 .find_map(|selection| {
325 let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
326 let project = project.read(cx);
327 let entry_id = project
328 .buffer_for_id(buffer_id, cx)?
329 .read(cx)
330 .entry_id(cx)?;
331 project.path_for_entry(entry_id, cx)
332 });
333 cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
334}
335
336fn run_flycheck_action(
337 editor: &mut Editor,
338 _: &RunFlycheck,
339 _: &mut Window,
340 cx: &mut Context<Editor>,
341) {
342 let Some(project) = &editor.project else {
343 return;
344 };
345 let buffer_id = editor
346 .selections
347 .disjoint_anchors_arc()
348 .iter()
349 .find_map(|selection| {
350 let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
351 let project = project.read(cx);
352 let entry_id = project
353 .buffer_for_id(buffer_id, cx)?
354 .read(cx)
355 .entry_id(cx)?;
356 project.path_for_entry(entry_id, cx)
357 });
358 run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
359}
360
361fn clear_flycheck_action(
362 editor: &mut Editor,
363 _: &ClearFlycheck,
364 _: &mut Window,
365 cx: &mut Context<Editor>,
366) {
367 let Some(project) = &editor.project else {
368 return;
369 };
370 let buffer_id = editor
371 .selections
372 .disjoint_anchors_arc()
373 .iter()
374 .find_map(|selection| {
375 let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
376 let project = project.read(cx);
377 let entry_id = project
378 .buffer_for_id(buffer_id, cx)?
379 .read(cx)
380 .entry_id(cx)?;
381 project.path_for_entry(entry_id, cx)
382 });
383 clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
384}