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 html_to_markdown::{convert_html_to_markdown, markdown, HandleTag};
9use http::{AsyncBody, HttpClient, HttpClientWithUrl};
10use language::LspAdapterDelegate;
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 let mut handlers: Vec<Box<dyn HandleTag>> = vec![
41 Box::new(markdown::ParagraphHandler),
42 Box::new(markdown::HeadingHandler),
43 Box::new(markdown::ListHandler),
44 Box::new(markdown::TableHandler::new()),
45 Box::new(markdown::StyledTextHandler),
46 ];
47 if url.contains("wikipedia.org") {
48 use html_to_markdown::structure::wikipedia;
49
50 handlers.push(Box::new(wikipedia::WikipediaChromeRemover));
51 handlers.push(Box::new(wikipedia::WikipediaInfoboxHandler));
52 handlers.push(Box::new(wikipedia::WikipediaCodeHandler::new()));
53 } else {
54 handlers.push(Box::new(markdown::CodeHandler));
55 }
56
57 convert_html_to_markdown(&body[..], handlers)
58 }
59}
60
61impl SlashCommand for FetchSlashCommand {
62 fn name(&self) -> String {
63 "fetch".into()
64 }
65
66 fn description(&self) -> String {
67 "insert URL contents".into()
68 }
69
70 fn menu_text(&self) -> String {
71 "Insert fetched URL contents".into()
72 }
73
74 fn requires_argument(&self) -> bool {
75 true
76 }
77
78 fn complete_argument(
79 &self,
80 _query: String,
81 _cancel: Arc<AtomicBool>,
82 _workspace: Option<WeakView<Workspace>>,
83 _cx: &mut AppContext,
84 ) -> Task<Result<Vec<String>>> {
85 Task::ready(Ok(Vec::new()))
86 }
87
88 fn run(
89 self: Arc<Self>,
90 argument: Option<&str>,
91 workspace: WeakView<Workspace>,
92 _delegate: Arc<dyn LspAdapterDelegate>,
93 cx: &mut WindowContext,
94 ) -> Task<Result<SlashCommandOutput>> {
95 let Some(argument) = argument else {
96 return Task::ready(Err(anyhow!("missing URL")));
97 };
98 let Some(workspace) = workspace.upgrade() else {
99 return Task::ready(Err(anyhow!("workspace was dropped")));
100 };
101
102 let http_client = workspace.read(cx).client().http_client();
103 let url = argument.to_string();
104
105 let text = cx.background_executor().spawn({
106 let url = url.clone();
107 async move { Self::build_message(http_client, &url).await }
108 });
109
110 let url = SharedString::from(url);
111 cx.foreground_executor().spawn(async move {
112 let text = text.await?;
113 let range = 0..text.len();
114 Ok(SlashCommandOutput {
115 text,
116 sections: vec![SlashCommandOutputSection {
117 range,
118 render_placeholder: Arc::new(move |id, unfold, _cx| {
119 FetchPlaceholder {
120 id,
121 unfold,
122 url: url.clone(),
123 }
124 .into_any_element()
125 }),
126 }],
127 run_commands_in_text: false,
128 })
129 })
130 }
131}
132
133#[derive(IntoElement)]
134struct FetchPlaceholder {
135 pub id: ElementId,
136 pub unfold: Arc<dyn Fn(&mut WindowContext)>,
137 pub url: SharedString,
138}
139
140impl RenderOnce for FetchPlaceholder {
141 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
142 let unfold = self.unfold;
143
144 ButtonLike::new(self.id)
145 .style(ButtonStyle::Filled)
146 .layer(ElevationIndex::ElevatedSurface)
147 .child(Icon::new(IconName::AtSign))
148 .child(Label::new(format!("fetch {url}", url = self.url)))
149 .on_click(move |_, cx| unfold(cx))
150 }
151}