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