rust_analyzer_ext.rs

  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}