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