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        module_path: Vec<String>,
 21    ) -> Result<String> {
 22        let version = "latest";
 23        let path = format!(
 24            "{crate_name}/{version}/{crate_name}/{module_path}",
 25            module_path = module_path.join("/")
 26        );
 27
 28        let mut response = http_client
 29            .get(
 30                &format!("https://docs.rs/{path}"),
 31                AsyncBody::default(),
 32                true,
 33            )
 34            .await?;
 35
 36        let mut body = Vec::new();
 37        response
 38            .body_mut()
 39            .read_to_end(&mut body)
 40            .await
 41            .context("error reading docs.rs response body")?;
 42
 43        if response.status().is_client_error() {
 44            let text = String::from_utf8_lossy(body.as_slice());
 45            bail!(
 46                "status error {}, response: {text:?}",
 47                response.status().as_u16()
 48            );
 49        }
 50
 51        convert_rustdoc_to_markdown(&body[..])
 52    }
 53}
 54
 55impl SlashCommand for RustdocSlashCommand {
 56    fn name(&self) -> String {
 57        "rustdoc".into()
 58    }
 59
 60    fn description(&self) -> String {
 61        "insert Rust docs".into()
 62    }
 63
 64    fn menu_text(&self) -> String {
 65        "Insert Rust Documentation".into()
 66    }
 67
 68    fn requires_argument(&self) -> bool {
 69        true
 70    }
 71
 72    fn complete_argument(
 73        &self,
 74        _query: String,
 75        _cancel: Arc<AtomicBool>,
 76        _workspace: WeakView<Workspace>,
 77        _cx: &mut AppContext,
 78    ) -> Task<Result<Vec<String>>> {
 79        Task::ready(Ok(Vec::new()))
 80    }
 81
 82    fn run(
 83        self: Arc<Self>,
 84        argument: Option<&str>,
 85        workspace: WeakView<Workspace>,
 86        _delegate: Arc<dyn LspAdapterDelegate>,
 87        cx: &mut WindowContext,
 88    ) -> Task<Result<SlashCommandOutput>> {
 89        let Some(argument) = argument else {
 90            return Task::ready(Err(anyhow!("missing crate name")));
 91        };
 92        let Some(workspace) = workspace.upgrade() else {
 93            return Task::ready(Err(anyhow!("workspace was dropped")));
 94        };
 95
 96        let http_client = workspace.read(cx).client().http_client();
 97        let mut path_components = argument.split("::");
 98        let crate_name = match path_components
 99            .next()
100            .ok_or_else(|| anyhow!("missing crate name"))
101        {
102            Ok(crate_name) => crate_name.to_string(),
103            Err(err) => return Task::ready(Err(err)),
104        };
105        let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
106
107        let text = cx.background_executor().spawn({
108            let crate_name = crate_name.clone();
109            let module_path = module_path.clone();
110            async move { Self::build_message(http_client, crate_name, module_path).await }
111        });
112
113        let crate_name = SharedString::from(crate_name);
114        let module_path = if module_path.is_empty() {
115            None
116        } else {
117            Some(SharedString::from(module_path.join("::")))
118        };
119        cx.foreground_executor().spawn(async move {
120            let text = text.await?;
121            let range = 0..text.len();
122            Ok(SlashCommandOutput {
123                text,
124                sections: vec![SlashCommandOutputSection {
125                    range,
126                    render_placeholder: Arc::new(move |id, unfold, _cx| {
127                        RustdocPlaceholder {
128                            id,
129                            unfold,
130                            crate_name: crate_name.clone(),
131                            module_path: module_path.clone(),
132                        }
133                        .into_any_element()
134                    }),
135                }],
136            })
137        })
138    }
139}
140
141#[derive(IntoElement)]
142struct RustdocPlaceholder {
143    pub id: ElementId,
144    pub unfold: Arc<dyn Fn(&mut WindowContext)>,
145    pub crate_name: SharedString,
146    pub module_path: Option<SharedString>,
147}
148
149impl RenderOnce for RustdocPlaceholder {
150    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
151        let unfold = self.unfold;
152
153        let crate_path = self
154            .module_path
155            .map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
156            .unwrap_or(self.crate_name.to_string());
157
158        ButtonLike::new(self.id)
159            .style(ButtonStyle::Filled)
160            .layer(ElevationIndex::ElevatedSurface)
161            .child(Icon::new(IconName::FileRust))
162            .child(Label::new(format!("rustdoc: {crate_path}")))
163            .on_click(move |_, cx| unfold(cx))
164    }
165}