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"
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)),
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.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)),
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.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!("Empty macro expansion for position {:?}", trigger_anchor);
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)),
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.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!("Empty docs urls for position {:?}", trigger_anchor);
288 return Ok(());
289 }
290
291 workspace.update(cx, |_workspace, cx| {
292 // Check if the local document exists, otherwise fallback to the online document.
293 // Open with the default browser.
294 if let Some(local_url) = docs_urls.local
295 && fs::metadata(Path::new(&local_url[8..])).is_ok()
296 {
297 cx.open_url(&local_url);
298 return;
299 }
300
301 if let Some(web_url) = docs_urls.web {
302 cx.open_url(&web_url);
303 }
304 });
305 anyhow::Ok(())
306 })
307 .detach_and_log_err(cx);
308}
309
310fn cancel_flycheck_action(
311 editor: &mut Editor,
312 _: &CancelFlycheck,
313 _: &mut Window,
314 cx: &mut Context<Editor>,
315) {
316 let Some(project) = &editor.project else {
317 return;
318 };
319 let multibuffer_snapshot = editor
320 .buffer
321 .read_with(cx, |buffer, cx| buffer.snapshot(cx));
322 let buffer_id = editor
323 .selections
324 .disjoint_anchors_arc()
325 .iter()
326 .find_map(|selection| {
327 let buffer_id = multibuffer_snapshot
328 .anchor_to_buffer_anchor(selection.start)?
329 .0
330 .buffer_id;
331 let project = project.read(cx);
332 let entry_id = project
333 .buffer_for_id(buffer_id, cx)?
334 .read(cx)
335 .entry_id(cx)?;
336 project.path_for_entry(entry_id, cx)
337 });
338 cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
339}
340
341fn run_flycheck_action(
342 editor: &mut Editor,
343 _: &RunFlycheck,
344 _: &mut Window,
345 cx: &mut Context<Editor>,
346) {
347 let Some(project) = &editor.project else {
348 return;
349 };
350 let multibuffer_snapshot = editor
351 .buffer
352 .read_with(cx, |buffer, cx| buffer.snapshot(cx));
353 let buffer_id = editor
354 .selections
355 .disjoint_anchors_arc()
356 .iter()
357 .find_map(|selection| {
358 let buffer_id = multibuffer_snapshot
359 .anchor_to_buffer_anchor(selection.head())?
360 .0
361 .buffer_id;
362 let project = project.read(cx);
363 let entry_id = project
364 .buffer_for_id(buffer_id, cx)?
365 .read(cx)
366 .entry_id(cx)?;
367 project.path_for_entry(entry_id, cx)
368 });
369 run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
370}
371
372fn clear_flycheck_action(
373 editor: &mut Editor,
374 _: &ClearFlycheck,
375 _: &mut Window,
376 cx: &mut Context<Editor>,
377) {
378 let Some(project) = &editor.project else {
379 return;
380 };
381 let multibuffer_snapshot = editor
382 .buffer
383 .read_with(cx, |buffer, cx| buffer.snapshot(cx));
384 let buffer_id = editor
385 .selections
386 .disjoint_anchors_arc()
387 .iter()
388 .find_map(|selection| {
389 let buffer_id = multibuffer_snapshot
390 .anchor_to_buffer_anchor(selection.head())?
391 .0
392 .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}