fetch_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 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}