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}