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, WeakModel, WeakView};
  8use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
  9use http_client::{AsyncBody, HttpClientWithUrl};
 10use picker::{Picker, PickerDelegate};
 11use ui::{prelude::*, ListItem, ViewContext};
 12use workspace::Workspace;
 13
 14use crate::context::ContextKind;
 15use crate::context_picker::ContextPicker;
 16use crate::context_store::ContextStore;
 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        context_store: WeakModel<ContextStore>,
 27        cx: &mut ViewContext<Self>,
 28    ) -> Self {
 29        let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
 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    context_store: WeakModel<ContextStore>,
 59    url: String,
 60}
 61
 62impl FetchContextPickerDelegate {
 63    pub fn new(
 64        context_picker: WeakView<ContextPicker>,
 65        workspace: WeakView<Workspace>,
 66        context_store: WeakModel<ContextStore>,
 67    ) -> Self {
 68        FetchContextPickerDelegate {
 69            context_picker,
 70            workspace,
 71            context_store,
 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        if self.url.is_empty() {
154            0
155        } else {
156            1
157        }
158    }
159
160    fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
161        "Enter the URL that you would like to fetch".into()
162    }
163
164    fn selected_index(&self) -> usize {
165        0
166    }
167
168    fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
169
170    fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
171        "Enter a URL…".into()
172    }
173
174    fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
175        self.url = query;
176
177        Task::ready(())
178    }
179
180    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
181        let Some(workspace) = self.workspace.upgrade() else {
182            return;
183        };
184
185        let http_client = workspace.read(cx).client().http_client().clone();
186        let url = self.url.clone();
187        cx.spawn(|this, mut cx| async move {
188            let text = Self::build_message(http_client, &url).await?;
189
190            this.update(&mut cx, |this, cx| {
191                this.delegate
192                    .context_store
193                    .update(cx, |context_store, _cx| {
194                        context_store.insert_context(ContextKind::FetchedUrl, url, text);
195                    })
196            })??;
197
198            anyhow::Ok(())
199        })
200        .detach_and_log_err(cx);
201    }
202
203    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
204        self.context_picker
205            .update(cx, |this, cx| {
206                this.reset_mode();
207                cx.emit(DismissEvent);
208            })
209            .ok();
210    }
211
212    fn render_match(
213        &self,
214        ix: usize,
215        selected: bool,
216        _cx: &mut ViewContext<Picker<Self>>,
217    ) -> Option<Self::ListItem> {
218        Some(
219            ListItem::new(ix)
220                .inset(true)
221                .toggle_state(selected)
222                .child(Label::new(self.url.clone())),
223        )
224    }
225}