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}