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}