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 server_lookup = find_specific_language_server_in_selection(
61 editor,
62 cx,
63 is_rust_language,
64 RUST_ANALYZER_NAME,
65 );
66
67 let project = project.clone();
68 let lsp_store = project.read(cx).lsp_store();
69 let upstream_client = lsp_store.read(cx).upstream_client();
70 cx.spawn_in(window, async move |editor, cx| {
71 let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
72 return anyhow::Ok(());
73 };
74
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 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 server_lookup = find_specific_language_server_in_selection(
143 editor,
144 cx,
145 is_rust_language,
146 RUST_ANALYZER_NAME,
147 );
148
149 let project = project.clone();
150 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
151 cx.spawn_in(window, async move |_editor, cx| {
152 let Some((trigger_anchor, rust_language, server_to_query, buffer)) = server_lookup.await
153 else {
154 return Ok(());
155 };
156
157 let macro_expansion = if let Some((client, project_id)) = upstream_client {
158 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
159 let request = proto::LspExtExpandMacro {
160 project_id,
161 buffer_id: buffer_id.to_proto(),
162 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
163 };
164 let response = client
165 .request(request)
166 .await
167 .context("lsp ext expand macro proto request")?;
168 ExpandedMacro {
169 name: response.name,
170 expansion: response.expansion,
171 }
172 } else {
173 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
174 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
175 project
176 .update(cx, |project, cx| {
177 project.request_lsp(
178 buffer,
179 project::LanguageServerToQuery::Other(server_to_query),
180 ExpandMacro { position },
181 cx,
182 )
183 })?
184 .await
185 .context("expand macro")?
186 };
187
188 if macro_expansion.is_empty() {
189 log::info!(
190 "Empty macro expansion for position {:?}",
191 trigger_anchor.text_anchor
192 );
193 return Ok(());
194 }
195
196 let buffer = project
197 .update(cx, |project, cx| project.create_buffer(cx))?
198 .await?;
199 workspace.update_in(cx, |workspace, window, cx| {
200 buffer.update(cx, |buffer, cx| {
201 buffer.set_text(macro_expansion.expansion, cx);
202 buffer.set_language(Some(rust_language), cx);
203 buffer.set_capability(Capability::ReadOnly, cx);
204 });
205 let multibuffer =
206 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
207 workspace.add_item_to_active_pane(
208 Box::new(cx.new(|cx| {
209 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
210 editor.set_read_only(true);
211 editor
212 })),
213 None,
214 true,
215 window,
216 cx,
217 );
218 })
219 })
220 .detach_and_log_err(cx);
221}
222
223pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mut Context<Editor>) {
224 if editor.selections.count() == 0 {
225 return;
226 }
227 let Some(project) = &editor.project else {
228 return;
229 };
230 let Some(workspace) = editor.workspace() else {
231 return;
232 };
233
234 let server_lookup = find_specific_language_server_in_selection(
235 editor,
236 cx,
237 is_rust_language,
238 RUST_ANALYZER_NAME,
239 );
240
241 let project = project.clone();
242 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
243 cx.spawn_in(window, async move |_editor, cx| {
244 let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
245 return Ok(());
246 };
247
248 let docs_urls = if let Some((client, project_id)) = upstream_client {
249 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?;
250 let request = proto::LspExtOpenDocs {
251 project_id,
252 buffer_id: buffer_id.to_proto(),
253 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
254 };
255 let response = client
256 .request(request)
257 .await
258 .context("lsp ext open docs proto request")?;
259 DocsUrls {
260 web: response.web,
261 local: response.local,
262 }
263 } else {
264 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
265 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
266 project
267 .update(cx, |project, cx| {
268 project.request_lsp(
269 buffer,
270 project::LanguageServerToQuery::Other(server_to_query),
271 project::lsp_store::lsp_ext_command::OpenDocs { position },
272 cx,
273 )
274 })?
275 .await
276 .context("open docs")?
277 };
278
279 if docs_urls.is_empty() {
280 log::debug!(
281 "Empty docs urls for position {:?}",
282 trigger_anchor.text_anchor
283 );
284 return Ok(());
285 }
286
287 workspace.update(cx, |_workspace, cx| {
288 // Check if the local document exists, otherwise fallback to the online document.
289 // Open with the default browser.
290 if let Some(local_url) = docs_urls.local {
291 if fs::metadata(Path::new(&local_url[8..])).is_ok() {
292 cx.open_url(&local_url);
293 return;
294 }
295 }
296
297 if let Some(web_url) = docs_urls.web {
298 cx.open_url(&web_url);
299 }
300 })
301 })
302 .detach_and_log_err(cx);
303}
304
305fn cancel_flycheck_action(
306 editor: &mut Editor,
307 _: &CancelFlycheck,
308 _: &mut Window,
309 cx: &mut Context<Editor>,
310) {
311 let Some(project) = &editor.project else {
312 return;
313 };
314 let Some(buffer_id) = editor
315 .selections
316 .disjoint_anchors()
317 .iter()
318 .find_map(|selection| {
319 let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
320 let project = project.read(cx);
321 let entry_id = project
322 .buffer_for_id(buffer_id, cx)?
323 .read(cx)
324 .entry_id(cx)?;
325 project.path_for_entry(entry_id, cx)
326 })
327 else {
328 return;
329 };
330 cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
331}
332
333fn run_flycheck_action(
334 editor: &mut Editor,
335 _: &RunFlycheck,
336 _: &mut Window,
337 cx: &mut Context<Editor>,
338) {
339 let Some(project) = &editor.project else {
340 return;
341 };
342 let Some(buffer_id) = editor
343 .selections
344 .disjoint_anchors()
345 .iter()
346 .find_map(|selection| {
347 let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
348 let project = project.read(cx);
349 let entry_id = project
350 .buffer_for_id(buffer_id, cx)?
351 .read(cx)
352 .entry_id(cx)?;
353 project.path_for_entry(entry_id, cx)
354 })
355 else {
356 return;
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 Some(buffer_id) = editor
371 .selections
372 .disjoint_anchors()
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 else {
384 return;
385 };
386 clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
387}