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 lsp_command::location_link_from_proto,
9 lsp_store::{
10 lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
11 rust_analyzer_ext::RUST_ANALYZER_NAME,
12 },
13};
14use rpc::proto;
15use text::ToPointUtf16;
16
17use crate::{
18 Editor, ExpandMacroRecursively, GoToParentModule, GotoDefinitionKind, OpenDocs,
19 element::register_action, hover_links::HoverLink,
20 lsp_ext::find_specific_language_server_in_selection,
21};
22
23fn is_rust_language(language: &Language) -> bool {
24 language.name() == "Rust".into()
25}
26
27pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
28 if editor
29 .read(cx)
30 .buffer()
31 .read(cx)
32 .all_buffers()
33 .into_iter()
34 .filter_map(|buffer| buffer.read(cx).language())
35 .any(|language| is_rust_language(language))
36 {
37 register_action(&editor, window, go_to_parent_module);
38 register_action(&editor, window, expand_macro_recursively);
39 register_action(&editor, window, open_docs);
40 }
41}
42
43pub fn go_to_parent_module(
44 editor: &mut Editor,
45 _: &GoToParentModule,
46 window: &mut Window,
47 cx: &mut Context<Editor>,
48) {
49 if editor.selections.count() == 0 {
50 return;
51 }
52 let Some(project) = &editor.project else {
53 return;
54 };
55
56 let server_lookup = find_specific_language_server_in_selection(
57 editor,
58 cx,
59 is_rust_language,
60 RUST_ANALYZER_NAME,
61 );
62
63 let project = project.clone();
64 let lsp_store = project.read(cx).lsp_store();
65 let upstream_client = lsp_store.read(cx).upstream_client();
66 cx.spawn_in(window, async move |editor, cx| {
67 let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
68 return anyhow::Ok(());
69 };
70
71 let location_links = if let Some((client, project_id)) = upstream_client {
72 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
73
74 let request = proto::LspExtGoToParentModule {
75 project_id,
76 buffer_id: buffer_id.to_proto(),
77 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
78 };
79 let response = client
80 .request(request)
81 .await
82 .context("lsp ext go to parent module proto request")?;
83 futures::future::join_all(
84 response
85 .links
86 .into_iter()
87 .map(|link| location_link_from_proto(link, lsp_store.clone(), cx)),
88 )
89 .await
90 .into_iter()
91 .collect::<anyhow::Result<_>>()
92 .context("go to parent module via collab")?
93 } else {
94 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
95 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
96 project
97 .update(cx, |project, cx| {
98 project.request_lsp(
99 buffer,
100 project::LanguageServerToQuery::Other(server_to_query),
101 project::lsp_store::lsp_ext_command::GoToParentModule { position },
102 cx,
103 )
104 })?
105 .await
106 .context("go to parent module")?
107 };
108
109 editor
110 .update_in(cx, |editor, window, cx| {
111 editor.navigate_to_hover_links(
112 Some(GotoDefinitionKind::Declaration),
113 location_links.into_iter().map(HoverLink::Text).collect(),
114 false,
115 window,
116 cx,
117 )
118 })?
119 .await?;
120 Ok(())
121 })
122 .detach_and_log_err(cx);
123}
124
125pub fn expand_macro_recursively(
126 editor: &mut Editor,
127 _: &ExpandMacroRecursively,
128 window: &mut Window,
129 cx: &mut Context<Editor>,
130) {
131 if editor.selections.count() == 0 {
132 return;
133 }
134 let Some(project) = &editor.project else {
135 return;
136 };
137 let Some(workspace) = editor.workspace() else {
138 return;
139 };
140
141 let server_lookup = find_specific_language_server_in_selection(
142 editor,
143 cx,
144 is_rust_language,
145 RUST_ANALYZER_NAME,
146 );
147
148 let project = project.clone();
149 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
150 cx.spawn_in(window, async move |_editor, cx| {
151 let Some((trigger_anchor, rust_language, server_to_query, buffer)) = server_lookup.await
152 else {
153 return Ok(());
154 };
155
156 let macro_expansion = if let Some((client, project_id)) = upstream_client {
157 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
158 let request = proto::LspExtExpandMacro {
159 project_id,
160 buffer_id: buffer_id.to_proto(),
161 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
162 };
163 let response = client
164 .request(request)
165 .await
166 .context("lsp ext expand macro proto request")?;
167 ExpandedMacro {
168 name: response.name,
169 expansion: response.expansion,
170 }
171 } else {
172 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
173 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
174 project
175 .update(cx, |project, cx| {
176 project.request_lsp(
177 buffer,
178 project::LanguageServerToQuery::Other(server_to_query),
179 ExpandMacro { position },
180 cx,
181 )
182 })?
183 .await
184 .context("expand macro")?
185 };
186
187 if macro_expansion.is_empty() {
188 log::info!(
189 "Empty macro expansion for position {:?}",
190 trigger_anchor.text_anchor
191 );
192 return Ok(());
193 }
194
195 let buffer = project
196 .update(cx, |project, cx| project.create_buffer(cx))?
197 .await?;
198 workspace.update_in(cx, |workspace, window, cx| {
199 buffer.update(cx, |buffer, cx| {
200 buffer.set_text(macro_expansion.expansion, cx);
201 buffer.set_language(Some(rust_language), cx);
202 buffer.set_capability(Capability::ReadOnly, cx);
203 });
204 let multibuffer =
205 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
206 workspace.add_item_to_active_pane(
207 Box::new(cx.new(|cx| {
208 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
209 editor.set_read_only(true);
210 editor
211 })),
212 None,
213 true,
214 window,
215 cx,
216 );
217 })
218 })
219 .detach_and_log_err(cx);
220}
221
222pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mut Context<Editor>) {
223 if editor.selections.count() == 0 {
224 return;
225 }
226 let Some(project) = &editor.project else {
227 return;
228 };
229 let Some(workspace) = editor.workspace() else {
230 return;
231 };
232
233 let server_lookup = find_specific_language_server_in_selection(
234 editor,
235 cx,
236 is_rust_language,
237 RUST_ANALYZER_NAME,
238 );
239
240 let project = project.clone();
241 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
242 cx.spawn_in(window, async move |_editor, cx| {
243 let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
244 return Ok(());
245 };
246
247 let docs_urls = if let Some((client, project_id)) = upstream_client {
248 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
249 let request = proto::LspExtOpenDocs {
250 project_id,
251 buffer_id: buffer_id.to_proto(),
252 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
253 };
254 let response = client
255 .request(request)
256 .await
257 .context("lsp ext open docs proto request")?;
258 DocsUrls {
259 web: response.web,
260 local: response.local,
261 }
262 } else {
263 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
264 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
265 project
266 .update(cx, |project, cx| {
267 project.request_lsp(
268 buffer,
269 project::LanguageServerToQuery::Other(server_to_query),
270 project::lsp_store::lsp_ext_command::OpenDocs { position },
271 cx,
272 )
273 })?
274 .await
275 .context("open docs")?
276 };
277
278 if docs_urls.is_empty() {
279 log::debug!(
280 "Empty docs urls for position {:?}",
281 trigger_anchor.text_anchor
282 );
283 return Ok(());
284 }
285
286 workspace.update(cx, |_workspace, cx| {
287 // Check if the local document exists, otherwise fallback to the online document.
288 // Open with the default browser.
289 if let Some(local_url) = docs_urls.local {
290 if fs::metadata(Path::new(&local_url[8..])).is_ok() {
291 cx.open_url(&local_url);
292 return;
293 }
294 }
295
296 if let Some(web_url) = docs_urls.web {
297 cx.open_url(&web_url);
298 }
299 })
300 })
301 .detach_and_log_err(cx);
302}