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}