218 lines · 6.8 KB · Rust
RawDownload
use std::cell::RefCell;use std::rc::Rc;use std::sync::Arc; use anyhow::{bail, Context as _, Result};use futures::AsyncReadExt as _;use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};use http_client::{AsyncBody, HttpClientWithUrl};use picker::{Picker, PickerDelegate};use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};use workspace::Workspace; use crate::context::ContextKind;use crate::context_picker::ContextPicker;use crate::message_editor::MessageEditor; pub struct FetchContextPicker {    picker: View<Picker<FetchContextPickerDelegate>>,} impl FetchContextPicker {    pub fn new(        context_picker: WeakView<ContextPicker>,        workspace: WeakView<Workspace>,        message_editor: WeakView<MessageEditor>,        cx: &mut ViewContext<Self>,    ) -> Self {        let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));         Self { picker }    }} impl FocusableView for FetchContextPicker {    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {        self.picker.focus_handle(cx)    }} impl Render for FetchContextPicker {    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {        self.picker.clone()    }} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]enum ContentType {    Html,    Plaintext,    Json,} pub struct FetchContextPickerDelegate {    context_picker: WeakView<ContextPicker>,    workspace: WeakView<Workspace>,    message_editor: WeakView<MessageEditor>,    url: String,} impl FetchContextPickerDelegate {    pub fn new(        context_picker: WeakView<ContextPicker>,        workspace: WeakView<Workspace>,        message_editor: WeakView<MessageEditor>,    ) -> Self {        FetchContextPickerDelegate {            context_picker,            workspace,            message_editor,            url: String::new(),        }    }     async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {        let mut url = url.to_owned();        if !url.starts_with("https://") && !url.starts_with("http://") {            url = format!("https://{url}");        }         let mut response = http_client.get(&url, AsyncBody::default(), true).await?;         let mut body = Vec::new();        response            .body_mut()            .read_to_end(&mut body)            .await            .context("error reading response body")?;         if response.status().is_client_error() {            let text = String::from_utf8_lossy(body.as_slice());            bail!(                "status error {}, response: {text:?}",                response.status().as_u16()            );        }         let Some(content_type) = response.headers().get("content-type") else {            bail!("missing Content-Type header");        };        let content_type = content_type            .to_str()            .context("invalid Content-Type header")?;        let content_type = match content_type {            "text/html" => ContentType::Html,            "text/plain" => ContentType::Plaintext,            "application/json" => ContentType::Json,            _ => ContentType::Html,        };         match content_type {            ContentType::Html => {                let mut handlers: Vec<TagHandler> = vec![                    Rc::new(RefCell::new(markdown::WebpageChromeRemover)),                    Rc::new(RefCell::new(markdown::ParagraphHandler)),                    Rc::new(RefCell::new(markdown::HeadingHandler)),                    Rc::new(RefCell::new(markdown::ListHandler)),                    Rc::new(RefCell::new(markdown::TableHandler::new())),                    Rc::new(RefCell::new(markdown::StyledTextHandler)),                ];                if url.contains("wikipedia.org") {                    use html_to_markdown::structure::wikipedia;                     handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));                    handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));                    handlers.push(Rc::new(                        RefCell::new(wikipedia::WikipediaCodeHandler::new()),                    ));                } else {                    handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));                }                 convert_html_to_markdown(&body[..], &mut handlers)            }            ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),            ContentType::Json => {                let json: serde_json::Value = serde_json::from_slice(&body)?;                 Ok(format!(                    "```json\n{}\n```",                    serde_json::to_string_pretty(&json)?                ))            }        }    }} impl PickerDelegate for FetchContextPickerDelegate {    type ListItem = ListItem;     fn match_count(&self) -> usize {        1    }     fn selected_index(&self) -> usize {        0    }     fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}     fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {        "Enter a URL…".into()    }     fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {        self.url = query;         Task::ready(())    }     fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {        let Some(workspace) = self.workspace.upgrade() else {            return;        };         let http_client = workspace.read(cx).client().http_client().clone();        let url = self.url.clone();        cx.spawn(|this, mut cx| async move {            let text = Self::build_message(http_client, &url).await?;             this.update(&mut cx, |this, cx| {                this.delegate                    .message_editor                    .update(cx, |message_editor, _cx| {                        message_editor.insert_context(ContextKind::FetchedUrl, url, text);                    })            })??;             anyhow::Ok(())        })        .detach_and_log_err(cx);    }     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {        self.context_picker            .update(cx, |this, cx| {                this.reset_mode();                cx.emit(DismissEvent);            })            .ok();    }     fn render_match(        &self,        ix: usize,        selected: bool,        _cx: &mut ViewContext<Picker<Self>>,    ) -> Option<Self::ListItem> {        Some(            ListItem::new(ix)                .inset(true)                .spacing(ListItemSpacing::Sparse)                .toggle_state(selected)                .child(self.url.clone()),        )    }}