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