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