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::lsp_store::{
8 lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
9 rust_analyzer_ext::RUST_ANALYZER_NAME,
10};
11use rpc::proto;
12use text::ToPointUtf16;
13
14use crate::{
15 Editor, ExpandMacroRecursively, OpenDocs, element::register_action,
16 lsp_ext::find_specific_language_server_in_selection,
17};
18
19fn is_rust_language(language: &Language) -> bool {
20 language.name() == "Rust".into()
21}
22
23pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
24 if editor
25 .read(cx)
26 .buffer()
27 .read(cx)
28 .all_buffers()
29 .into_iter()
30 .filter_map(|buffer| buffer.read(cx).language())
31 .any(|language| is_rust_language(language))
32 {
33 register_action(&editor, window, expand_macro_recursively);
34 register_action(&editor, window, open_docs);
35 }
36}
37
38pub fn expand_macro_recursively(
39 editor: &mut Editor,
40 _: &ExpandMacroRecursively,
41 window: &mut Window,
42 cx: &mut Context<Editor>,
43) {
44 if editor.selections.count() == 0 {
45 return;
46 }
47 let Some(project) = &editor.project else {
48 return;
49 };
50 let Some(workspace) = editor.workspace() else {
51 return;
52 };
53
54 let server_lookup = find_specific_language_server_in_selection(
55 editor,
56 cx,
57 is_rust_language,
58 RUST_ANALYZER_NAME,
59 );
60
61 let project = project.clone();
62 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
63 cx.spawn_in(window, async move |_editor, cx| {
64 let Some((trigger_anchor, rust_language, server_to_query, buffer)) = server_lookup.await
65 else {
66 return Ok(());
67 };
68
69 let macro_expansion = if let Some((client, project_id)) = upstream_client {
70 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
71 let request = proto::LspExtExpandMacro {
72 project_id,
73 buffer_id: buffer_id.to_proto(),
74 position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
75 };
76 let response = client
77 .request(request)
78 .await
79 .context("lsp ext expand macro proto request")?;
80 ExpandedMacro {
81 name: response.name,
82 expansion: response.expansion,
83 }
84 } else {
85 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
86 let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
87 project
88 .update(cx, |project, cx| {
89 project.request_lsp(
90 buffer,
91 project::LanguageServerToQuery::Other(server_to_query),
92 ExpandMacro { position },
93 cx,
94 )
95 })?
96 .await
97 .context("expand macro")?
98 };
99
100 if macro_expansion.is_empty() {
101 log::info!(
102 "Empty macro expansion for position {:?}",
103 trigger_anchor.text_anchor
104 );
105 return Ok(());
106 }
107
108 let buffer = project
109 .update(cx, |project, cx| project.create_buffer(cx))?
110 .await?;
111 workspace.update_in(cx, |workspace, window, cx| {
112 buffer.update(cx, |buffer, cx| {
113 buffer.set_text(macro_expansion.expansion, cx);
114 buffer.set_language(Some(rust_language), cx);
115 buffer.set_capability(Capability::ReadOnly, cx);
116 });
117 let multibuffer =
118 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
119 workspace.add_item_to_active_pane(
120 Box::new(cx.new(|cx| {
121 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
122 editor.set_read_only(true);
123 editor
124 })),
125 None,
126 true,
127 window,
128 cx,
129 );
130 })
131 })
132 .detach_and_log_err(cx);
133}
134
135pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mut Context<Editor>) {
136 if editor.selections.count() == 0 {
137 return;
138 }
139 let Some(project) = &editor.project else {
140 return;
141 };
142 let Some(workspace) = editor.workspace() else {
143 return;
144 };
145
146 let server_lookup = find_specific_language_server_in_selection(
147 editor,
148 cx,
149 is_rust_language,
150 RUST_ANALYZER_NAME,
151 );
152
153 let project = project.clone();
154 let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
155 cx.spawn_in(window, async move |_editor, cx| {
156 let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
157 return Ok(());
158 };
159
160 let docs_urls = if let Some((client, project_id)) = upstream_client {
161 let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
162 let request = proto::LspExtOpenDocs {
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 open docs proto request")?;
171 DocsUrls {
172 web: response.web,
173 local: response.local,
174 }
175 } else {
176 let buffer_snapshot = buffer.update(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 project::lsp_store::lsp_ext_command::OpenDocs { position },
184 cx,
185 )
186 })?
187 .await
188 .context("open docs")?
189 };
190
191 if docs_urls.is_empty() {
192 log::debug!(
193 "Empty docs urls for position {:?}",
194 trigger_anchor.text_anchor
195 );
196 return Ok(());
197 }
198
199 workspace.update(cx, |_workspace, cx| {
200 // Check if the local document exists, otherwise fallback to the online document.
201 // Open with the default browser.
202 if let Some(local_url) = docs_urls.local {
203 if fs::metadata(Path::new(&local_url[8..])).is_ok() {
204 cx.open_url(&local_url);
205 return;
206 }
207 }
208
209 if let Some(web_url) = docs_urls.web {
210 cx.open_url(&web_url);
211 }
212 })
213 })
214 .detach_and_log_err(cx);
215}