fetch_context_picker.rs

  1use std::cell::RefCell;
  2use std::rc::Rc;
  3use std::sync::Arc;
  4
  5use anyhow::{bail, Context as _, Result};
  6use futures::AsyncReadExt as _;
  7use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
  8use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
  9use http_client::{AsyncBody, HttpClientWithUrl};
 10use picker::{Picker, PickerDelegate};
 11use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
 12use workspace::Workspace;
 13
 14use crate::context::ContextKind;
 15use crate::context_picker::ContextPicker;
 16use crate::message_editor::MessageEditor;
 17
 18pub struct FetchContextPicker {
 19    picker: View<Picker<FetchContextPickerDelegate>>,
 20}
 21
 22impl FetchContextPicker {
 23    pub fn new(
 24        context_picker: WeakView<ContextPicker>,
 25        workspace: WeakView<Workspace>,
 26        message_editor: WeakView<MessageEditor>,
 27        cx: &mut ViewContext<Self>,
 28    ) -> Self {
 29        let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
 30        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
 31
 32        Self { picker }
 33    }
 34}
 35
 36impl FocusableView for FetchContextPicker {
 37    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 38        self.picker.focus_handle(cx)
 39    }
 40}
 41
 42impl Render for FetchContextPicker {
 43    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 44        self.picker.clone()
 45    }
 46}
 47
 48#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 49enum ContentType {
 50    Html,
 51    Plaintext,
 52    Json,
 53}
 54
 55pub struct FetchContextPickerDelegate {
 56    context_picker: WeakView<ContextPicker>,
 57    workspace: WeakView<Workspace>,
 58    message_editor: WeakView<MessageEditor>,
 59    url: String,
 60}
 61
 62impl FetchContextPickerDelegate {
 63    pub fn new(
 64        context_picker: WeakView<ContextPicker>,
 65        workspace: WeakView<Workspace>,
 66        message_editor: WeakView<MessageEditor>,
 67    ) -> Self {
 68        FetchContextPickerDelegate {
 69            context_picker,
 70            workspace,
 71            message_editor,
 72            url: String::new(),
 73        }
 74    }
 75
 76    async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
 77        let mut url = url.to_owned();
 78        if !url.starts_with("https://") && !url.starts_with("http://") {
 79            url = format!("https://{url}");
 80        }
 81
 82        let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
 83
 84        let mut body = Vec::new();
 85        response
 86            .body_mut()
 87            .read_to_end(&mut body)
 88            .await
 89            .context("error reading response body")?;
 90
 91        if response.status().is_client_error() {
 92            let text = String::from_utf8_lossy(body.as_slice());
 93            bail!(
 94                "status error {}, response: {text:?}",
 95                response.status().as_u16()
 96            );
 97        }
 98
 99        let Some(content_type) = response.headers().get("content-type") else {
100            bail!("missing Content-Type header");
101        };
102        let content_type = content_type
103            .to_str()
104            .context("invalid Content-Type header")?;
105        let content_type = match content_type {
106            "text/html" => ContentType::Html,
107            "text/plain" => ContentType::Plaintext,
108            "application/json" => ContentType::Json,
109            _ => ContentType::Html,
110        };
111
112        match content_type {
113            ContentType::Html => {
114                let mut handlers: Vec<TagHandler> = vec![
115                    Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
116                    Rc::new(RefCell::new(markdown::ParagraphHandler)),
117                    Rc::new(RefCell::new(markdown::HeadingHandler)),
118                    Rc::new(RefCell::new(markdown::ListHandler)),
119                    Rc::new(RefCell::new(markdown::TableHandler::new())),
120                    Rc::new(RefCell::new(markdown::StyledTextHandler)),
121                ];
122                if url.contains("wikipedia.org") {
123                    use html_to_markdown::structure::wikipedia;
124
125                    handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
126                    handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
127                    handlers.push(Rc::new(
128                        RefCell::new(wikipedia::WikipediaCodeHandler::new()),
129                    ));
130                } else {
131                    handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
132                }
133
134                convert_html_to_markdown(&body[..], &mut handlers)
135            }
136            ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
137            ContentType::Json => {
138                let json: serde_json::Value = serde_json::from_slice(&body)?;
139
140                Ok(format!(
141                    "```json\n{}\n```",
142                    serde_json::to_string_pretty(&json)?
143                ))
144            }
145        }
146    }
147}
148
149impl PickerDelegate for FetchContextPickerDelegate {
150    type ListItem = ListItem;
151
152    fn match_count(&self) -> usize {
153        1
154    }
155
156    fn selected_index(&self) -> usize {
157        0
158    }
159
160    fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
161
162    fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
163        "Enter a URL…".into()
164    }
165
166    fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
167        self.url = query;
168
169        Task::ready(())
170    }
171
172    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
173        let Some(workspace) = self.workspace.upgrade() else {
174            return;
175        };
176
177        let http_client = workspace.read(cx).client().http_client().clone();
178        let url = self.url.clone();
179        cx.spawn(|this, mut cx| async move {
180            let text = Self::build_message(http_client, &url).await?;
181
182            this.update(&mut cx, |this, cx| {
183                this.delegate
184                    .message_editor
185                    .update(cx, |message_editor, _cx| {
186                        message_editor.insert_context(ContextKind::FetchedUrl, url, text);
187                    })
188            })??;
189
190            anyhow::Ok(())
191        })
192        .detach_and_log_err(cx);
193    }
194
195    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
196        self.context_picker
197            .update(cx, |this, cx| {
198                this.reset_mode();
199                cx.emit(DismissEvent);
200            })
201            .ok();
202    }
203
204    fn render_match(
205        &self,
206        ix: usize,
207        selected: bool,
208        _cx: &mut ViewContext<Picker<Self>>,
209    ) -> Option<Self::ListItem> {
210        Some(
211            ListItem::new(ix)
212                .inset(true)
213                .spacing(ListItemSpacing::Sparse)
214                .toggle_state(selected)
215                .child(self.url.clone()),
216        )
217    }
218}