rustdoc_command.rs

  1use std::sync::atomic::AtomicBool;
  2use std::sync::Arc;
  3
  4use anyhow::{anyhow, bail, Context, Result};
  5use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
  6use futures::AsyncReadExt;
  7use gpui::{AppContext, Task, WeakView};
  8use http::{AsyncBody, HttpClient, HttpClientWithUrl};
  9use language::LspAdapterDelegate;
 10use rustdoc_to_markdown::convert_rustdoc_to_markdown;
 11use ui::{prelude::*, ButtonLike, ElevationIndex};
 12use workspace::Workspace;
 13
 14pub(crate) struct RustdocSlashCommand;
 15
 16impl RustdocSlashCommand {
 17    async fn build_message(
 18        http_client: Arc<HttpClientWithUrl>,
 19        crate_name: String,
 20    ) -> Result<String> {
 21        let mut response = http_client
 22            .get(
 23                &format!("https://docs.rs/{crate_name}"),
 24                AsyncBody::default(),
 25                true,
 26            )
 27            .await?;
 28
 29        let mut body = Vec::new();
 30        response
 31            .body_mut()
 32            .read_to_end(&mut body)
 33            .await
 34            .context("error reading docs.rs response body")?;
 35
 36        if response.status().is_client_error() {
 37            let text = String::from_utf8_lossy(body.as_slice());
 38            bail!(
 39                "status error {}, response: {text:?}",
 40                response.status().as_u16()
 41            );
 42        }
 43
 44        convert_rustdoc_to_markdown(&body[..])
 45    }
 46}
 47
 48impl SlashCommand for RustdocSlashCommand {
 49    fn name(&self) -> String {
 50        "rustdoc".into()
 51    }
 52
 53    fn description(&self) -> String {
 54        "insert the docs for a Rust crate".into()
 55    }
 56
 57    fn tooltip_text(&self) -> String {
 58        "insert rustdoc".into()
 59    }
 60
 61    fn requires_argument(&self) -> bool {
 62        true
 63    }
 64
 65    fn complete_argument(
 66        &self,
 67        _query: String,
 68        _cancel: Arc<AtomicBool>,
 69        _workspace: WeakView<Workspace>,
 70        _cx: &mut AppContext,
 71    ) -> Task<Result<Vec<String>>> {
 72        Task::ready(Ok(Vec::new()))
 73    }
 74
 75    fn run(
 76        self: Arc<Self>,
 77        argument: Option<&str>,
 78        workspace: WeakView<Workspace>,
 79        _delegate: Arc<dyn LspAdapterDelegate>,
 80        cx: &mut WindowContext,
 81    ) -> Task<Result<SlashCommandOutput>> {
 82        let Some(argument) = argument else {
 83            return Task::ready(Err(anyhow!("missing crate name")));
 84        };
 85        let Some(workspace) = workspace.upgrade() else {
 86            return Task::ready(Err(anyhow!("workspace was dropped")));
 87        };
 88
 89        let http_client = workspace.read(cx).client().http_client();
 90        let crate_name = argument.to_string();
 91
 92        let text = cx.background_executor().spawn({
 93            let crate_name = crate_name.clone();
 94            async move { Self::build_message(http_client, crate_name).await }
 95        });
 96
 97        let crate_name = SharedString::from(crate_name);
 98        cx.foreground_executor().spawn(async move {
 99            let text = text.await?;
100            let range = 0..text.len();
101            Ok(SlashCommandOutput {
102                text,
103                sections: vec![SlashCommandOutputSection {
104                    range,
105                    render_placeholder: Arc::new(move |id, unfold, _cx| {
106                        RustdocPlaceholder {
107                            id,
108                            unfold,
109                            crate_name: crate_name.clone(),
110                        }
111                        .into_any_element()
112                    }),
113                }],
114            })
115        })
116    }
117}
118
119#[derive(IntoElement)]
120struct RustdocPlaceholder {
121    pub id: ElementId,
122    pub unfold: Arc<dyn Fn(&mut WindowContext)>,
123    pub crate_name: SharedString,
124}
125
126impl RenderOnce for RustdocPlaceholder {
127    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
128        let unfold = self.unfold;
129
130        ButtonLike::new(self.id)
131            .style(ButtonStyle::Filled)
132            .layer(ElevationIndex::ElevatedSurface)
133            .child(Icon::new(IconName::FileRust))
134            .child(Label::new(format!("rustdoc: {}", self.crate_name)))
135            .on_click(move |_, cx| unfold(cx))
136    }
137}