1use acp_thread::{MentionUri, selection_name};
2use agent::{HistoryStore, outline};
3use agent_client_protocol as acp;
4use agent_servers::{AgentServer, AgentServerDelegate};
5use anyhow::{Context as _, Result, anyhow};
6use assistant_slash_commands::codeblock_fence_for_path;
7use collections::{HashMap, HashSet};
8use editor::{
9 Anchor, Editor, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
10 display_map::{Crease, CreaseId, CreaseMetadata, FoldId},
11 scroll::Autoscroll,
12};
13use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
14use gpui::{
15 AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,
16 SharedString, Task, WeakEntity,
17};
18use http_client::{AsyncBody, HttpClientWithUrl};
19use itertools::Either;
20use language::Buffer;
21use language_model::LanguageModelImage;
22use multi_buffer::MultiBufferRow;
23use postage::stream::Stream as _;
24use project::{Project, ProjectItem, ProjectPath, Worktree};
25use prompt_store::{PromptId, PromptStore};
26use rope::Point;
27use std::{
28 cell::RefCell,
29 ffi::OsStr,
30 fmt::Write,
31 ops::{Range, RangeInclusive},
32 path::{Path, PathBuf},
33 rc::Rc,
34 sync::Arc,
35};
36use text::OffsetRangeExt;
37use ui::{Disclosure, Toggleable, prelude::*};
38use util::{ResultExt, debug_panic, rel_path::RelPath};
39use workspace::{Workspace, notifications::NotifyResultExt as _};
40
41use crate::ui::MentionCrease;
42
43pub type MentionTask = Shared<Task<Result<Mention, String>>>;
44
45#[derive(Debug, Clone, Eq, PartialEq)]
46pub enum Mention {
47 Text {
48 content: String,
49 tracked_buffers: Vec<Entity<Buffer>>,
50 },
51 Image(MentionImage),
52 Link,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct MentionImage {
57 pub data: SharedString,
58 pub format: ImageFormat,
59}
60
61pub struct MentionSet {
62 project: WeakEntity<Project>,
63 history_store: Entity<HistoryStore>,
64 prompt_store: Option<Entity<PromptStore>>,
65 mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
66}
67
68impl MentionSet {
69 pub fn new(
70 project: WeakEntity<Project>,
71 history_store: Entity<HistoryStore>,
72 prompt_store: Option<Entity<PromptStore>>,
73 ) -> Self {
74 Self {
75 project,
76 history_store,
77 prompt_store,
78 mentions: HashMap::default(),
79 }
80 }
81
82 pub fn contents(
83 &self,
84 full_mention_content: bool,
85 cx: &mut App,
86 ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
87 let Some(project) = self.project.upgrade() else {
88 return Task::ready(Err(anyhow!("Project not found")));
89 };
90 let mentions = self.mentions.clone();
91 cx.spawn(async move |cx| {
92 let mut contents = HashMap::default();
93 for (crease_id, (mention_uri, task)) in mentions {
94 let content = if full_mention_content
95 && let MentionUri::Directory { abs_path } = &mention_uri
96 {
97 cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
98 .await?
99 } else {
100 task.await.map_err(|e| anyhow!("{e}"))?
101 };
102
103 contents.insert(crease_id, (mention_uri, content));
104 }
105 Ok(contents)
106 })
107 }
108
109 pub fn remove_invalid(&mut self, snapshot: &EditorSnapshot) {
110 for (crease_id, crease) in snapshot.crease_snapshot.creases() {
111 if !crease.range().start.is_valid(snapshot.buffer_snapshot()) {
112 self.mentions.remove(&crease_id);
113 }
114 }
115 }
116
117 pub fn insert_mention(&mut self, crease_id: CreaseId, uri: MentionUri, task: MentionTask) {
118 self.mentions.insert(crease_id, (uri, task));
119 }
120
121 pub fn remove_mention(&mut self, crease_id: &CreaseId) {
122 self.mentions.remove(crease_id);
123 }
124
125 pub fn creases(&self) -> HashSet<CreaseId> {
126 self.mentions.keys().cloned().collect()
127 }
128
129 pub fn mentions(&self) -> HashSet<MentionUri> {
130 self.mentions.values().map(|(uri, _)| uri.clone()).collect()
131 }
132
133 pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {
134 self.mentions = mentions;
135 }
136
137 pub fn clear(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
138 self.mentions.drain()
139 }
140
141 pub fn confirm_mention_completion(
142 &mut self,
143 crease_text: SharedString,
144 start: text::Anchor,
145 content_len: usize,
146 mention_uri: MentionUri,
147 supports_images: bool,
148 editor: Entity<Editor>,
149 workspace: &Entity<Workspace>,
150 window: &mut Window,
151 cx: &mut Context<Self>,
152 ) -> Task<()> {
153 let Some(project) = self.project.upgrade() else {
154 return Task::ready(());
155 };
156
157 let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
158 let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
159 return Task::ready(());
160 };
161 let excerpt_id = start_anchor.excerpt_id;
162 let end_anchor = snapshot.buffer_snapshot().anchor_before(
163 start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
164 );
165
166 let crease = if let MentionUri::File { abs_path } = &mention_uri
167 && let Some(extension) = abs_path.extension()
168 && let Some(extension) = extension.to_str()
169 && Img::extensions().contains(&extension)
170 && !extension.contains("svg")
171 {
172 let Some(project_path) = project
173 .read(cx)
174 .project_path_for_absolute_path(&abs_path, cx)
175 else {
176 log::error!("project path not found");
177 return Task::ready(());
178 };
179 let image_task = project.update(cx, |project, cx| project.open_image(project_path, cx));
180 let image = cx
181 .spawn(async move |_, cx| {
182 let image = image_task.await.map_err(|e| e.to_string())?;
183 let image = image
184 .update(cx, |image, _| image.image.clone())
185 .map_err(|e| e.to_string())?;
186 Ok(image)
187 })
188 .shared();
189 insert_crease_for_mention(
190 excerpt_id,
191 start,
192 content_len,
193 mention_uri.name().into(),
194 IconName::Image.path().into(),
195 Some(image),
196 editor.clone(),
197 window,
198 cx,
199 )
200 } else {
201 insert_crease_for_mention(
202 excerpt_id,
203 start,
204 content_len,
205 crease_text,
206 mention_uri.icon_path(cx),
207 None,
208 editor.clone(),
209 window,
210 cx,
211 )
212 };
213 let Some((crease_id, tx)) = crease else {
214 return Task::ready(());
215 };
216
217 let task = match mention_uri.clone() {
218 MentionUri::Fetch { url } => {
219 self.confirm_mention_for_fetch(url, workspace.read(cx).client().http_client(), cx)
220 }
221 MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
222 MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
223 MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
224 MentionUri::File { abs_path } => {
225 self.confirm_mention_for_file(abs_path, supports_images, cx)
226 }
227 MentionUri::Symbol {
228 abs_path,
229 line_range,
230 ..
231 } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
232 MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
233 MentionUri::PastedImage => {
234 debug_panic!("pasted image URI should not be included in completions");
235 Task::ready(Err(anyhow!(
236 "pasted imaged URI should not be included in completions"
237 )))
238 }
239 MentionUri::Selection { .. } => {
240 debug_panic!("unexpected selection URI");
241 Task::ready(Err(anyhow!("unexpected selection URI")))
242 }
243 };
244 let task = cx
245 .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
246 .shared();
247 self.mentions.insert(crease_id, (mention_uri, task.clone()));
248
249 // Notify the user if we failed to load the mentioned context
250 cx.spawn_in(window, async move |this, cx| {
251 let result = task.await.notify_async_err(cx);
252 drop(tx);
253 if result.is_none() {
254 this.update(cx, |this, cx| {
255 editor.update(cx, |editor, cx| {
256 // Remove mention
257 editor.edit([(start_anchor..end_anchor, "")], cx);
258 });
259 this.mentions.remove(&crease_id);
260 })
261 .ok();
262 }
263 })
264 }
265
266 pub fn confirm_mention_for_file(
267 &self,
268 abs_path: PathBuf,
269 supports_images: bool,
270 cx: &mut Context<Self>,
271 ) -> Task<Result<Mention>> {
272 let Some(project) = self.project.upgrade() else {
273 return Task::ready(Err(anyhow!("project not found")));
274 };
275
276 let Some(project_path) = project
277 .read(cx)
278 .project_path_for_absolute_path(&abs_path, cx)
279 else {
280 return Task::ready(Err(anyhow!("project path not found")));
281 };
282 let extension = abs_path
283 .extension()
284 .and_then(OsStr::to_str)
285 .unwrap_or_default();
286
287 if Img::extensions().contains(&extension) && !extension.contains("svg") {
288 if !supports_images {
289 return Task::ready(Err(anyhow!("This model does not support images yet")));
290 }
291 let task = project.update(cx, |project, cx| project.open_image(project_path, cx));
292 return cx.spawn(async move |_, cx| {
293 let image = task.await?;
294 let image = image.update(cx, |image, _| image.image.clone())?;
295 let format = image.format;
296 let image = cx
297 .update(|cx| LanguageModelImage::from_image(image, cx))?
298 .await;
299 if let Some(image) = image {
300 Ok(Mention::Image(MentionImage {
301 data: image.source,
302 format,
303 }))
304 } else {
305 Err(anyhow!("Failed to convert image"))
306 }
307 });
308 }
309
310 let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
311 cx.spawn(async move |_, cx| {
312 let buffer = buffer.await?;
313 let buffer_content = outline::get_buffer_content_or_outline(
314 buffer.clone(),
315 Some(&abs_path.to_string_lossy()),
316 &cx,
317 )
318 .await?;
319
320 Ok(Mention::Text {
321 content: buffer_content.text,
322 tracked_buffers: vec![buffer],
323 })
324 })
325 }
326
327 fn confirm_mention_for_fetch(
328 &self,
329 url: url::Url,
330 http_client: Arc<HttpClientWithUrl>,
331 cx: &mut Context<Self>,
332 ) -> Task<Result<Mention>> {
333 cx.background_executor().spawn(async move {
334 let content = fetch_url_content(http_client, url.to_string()).await?;
335 Ok(Mention::Text {
336 content,
337 tracked_buffers: Vec::new(),
338 })
339 })
340 }
341
342 fn confirm_mention_for_symbol(
343 &self,
344 abs_path: PathBuf,
345 line_range: RangeInclusive<u32>,
346 cx: &mut Context<Self>,
347 ) -> Task<Result<Mention>> {
348 let Some(project) = self.project.upgrade() else {
349 return Task::ready(Err(anyhow!("project not found")));
350 };
351 let Some(project_path) = project
352 .read(cx)
353 .project_path_for_absolute_path(&abs_path, cx)
354 else {
355 return Task::ready(Err(anyhow!("project path not found")));
356 };
357 let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
358 cx.spawn(async move |_, cx| {
359 let buffer = buffer.await?;
360 let mention = buffer.update(cx, |buffer, cx| {
361 let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
362 let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
363 let content = buffer.text_for_range(start..end).collect();
364 Mention::Text {
365 content,
366 tracked_buffers: vec![cx.entity()],
367 }
368 })?;
369 anyhow::Ok(mention)
370 })
371 }
372
373 fn confirm_mention_for_rule(
374 &mut self,
375 id: PromptId,
376 cx: &mut Context<Self>,
377 ) -> Task<Result<Mention>> {
378 let Some(prompt_store) = self.prompt_store.as_ref() else {
379 return Task::ready(Err(anyhow!("Missing prompt store")));
380 };
381 let prompt = prompt_store.read(cx).load(id, cx);
382 cx.spawn(async move |_, _| {
383 let prompt = prompt.await?;
384 Ok(Mention::Text {
385 content: prompt,
386 tracked_buffers: Vec::new(),
387 })
388 })
389 }
390
391 pub fn confirm_mention_for_selection(
392 &mut self,
393 source_range: Range<text::Anchor>,
394 selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
395 editor: Entity<Editor>,
396 window: &mut Window,
397 cx: &mut Context<Self>,
398 ) {
399 let Some(project) = self.project.upgrade() else {
400 return;
401 };
402
403 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
404 let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
405 return;
406 };
407
408 let offset = start.to_offset(&snapshot);
409
410 for (buffer, selection_range, range_to_fold) in selections {
411 let range = snapshot.anchor_after(offset + range_to_fold.start)
412 ..snapshot.anchor_after(offset + range_to_fold.end);
413
414 let abs_path = buffer
415 .read(cx)
416 .project_path(cx)
417 .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx));
418 let snapshot = buffer.read(cx).snapshot();
419
420 let text = snapshot
421 .text_for_range(selection_range.clone())
422 .collect::<String>();
423 let point_range = selection_range.to_point(&snapshot);
424 let line_range = point_range.start.row..=point_range.end.row;
425
426 let uri = MentionUri::Selection {
427 abs_path: abs_path.clone(),
428 line_range: line_range.clone(),
429 };
430 let crease = crease_for_mention(
431 selection_name(abs_path.as_deref(), &line_range).into(),
432 uri.icon_path(cx),
433 range,
434 editor.downgrade(),
435 );
436
437 let crease_id = editor.update(cx, |editor, cx| {
438 let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
439 editor.fold_creases(vec![crease], false, window, cx);
440 crease_ids.first().copied().unwrap()
441 });
442
443 self.mentions.insert(
444 crease_id,
445 (
446 uri,
447 Task::ready(Ok(Mention::Text {
448 content: text,
449 tracked_buffers: vec![buffer],
450 }))
451 .shared(),
452 ),
453 );
454 }
455
456 // Take this explanation with a grain of salt but, with creases being
457 // inserted, GPUI's recomputes the editor layout in the next frames, so
458 // directly calling `editor.request_autoscroll` wouldn't work as
459 // expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
460 // ensure that the layout has been recalculated so that the autoscroll
461 // request actually shows the cursor's new position.
462 cx.on_next_frame(window, move |_, window, cx| {
463 cx.on_next_frame(window, move |_, _, cx| {
464 editor.update(cx, |editor, cx| {
465 editor.request_autoscroll(Autoscroll::fit(), cx)
466 });
467 });
468 });
469 }
470
471 fn confirm_mention_for_thread(
472 &mut self,
473 id: acp::SessionId,
474 cx: &mut Context<Self>,
475 ) -> Task<Result<Mention>> {
476 let Some(project) = self.project.upgrade() else {
477 return Task::ready(Err(anyhow!("project not found")));
478 };
479
480 let server = Rc::new(agent::NativeAgentServer::new(
481 project.read(cx).fs().clone(),
482 self.history_store.clone(),
483 ));
484 let delegate = AgentServerDelegate::new(
485 project.read(cx).agent_server_store().clone(),
486 project.clone(),
487 None,
488 None,
489 );
490 let connection = server.connect(None, delegate, cx);
491 cx.spawn(async move |_, cx| {
492 let (agent, _) = connection.await?;
493 let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
494 let summary = agent
495 .0
496 .update(cx, |agent, cx| agent.thread_summary(id, cx))?
497 .await?;
498 anyhow::Ok(Mention::Text {
499 content: summary.to_string(),
500 tracked_buffers: Vec::new(),
501 })
502 })
503 }
504
505 fn confirm_mention_for_text_thread(
506 &mut self,
507 path: PathBuf,
508 cx: &mut Context<Self>,
509 ) -> Task<Result<Mention>> {
510 let text_thread_task = self.history_store.update(cx, |store, cx| {
511 store.load_text_thread(path.as_path().into(), cx)
512 });
513 cx.spawn(async move |_, cx| {
514 let text_thread = text_thread_task.await?;
515 let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
516 Ok(Mention::Text {
517 content: xml,
518 tracked_buffers: Vec::new(),
519 })
520 })
521 }
522}
523
524pub(crate) fn paste_images_as_context(
525 editor: Entity<Editor>,
526 mention_set: Entity<MentionSet>,
527 window: &mut Window,
528 cx: &mut App,
529) -> Option<Task<()>> {
530 let clipboard = cx.read_from_clipboard()?;
531 Some(window.spawn(cx, async move |cx| {
532 use itertools::Itertools;
533 let (mut images, paths) = clipboard
534 .into_entries()
535 .filter_map(|entry| match entry {
536 ClipboardEntry::Image(image) => Some(Either::Left(image)),
537 ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
538 _ => None,
539 })
540 .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
541
542 if !paths.is_empty() {
543 images.extend(
544 cx.background_spawn(async move {
545 let mut images = vec![];
546 for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
547 let Ok(content) = async_fs::read(path).await else {
548 continue;
549 };
550 let Ok(format) = image::guess_format(&content) else {
551 continue;
552 };
553 images.push(gpui::Image::from_bytes(
554 match format {
555 image::ImageFormat::Png => gpui::ImageFormat::Png,
556 image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
557 image::ImageFormat::WebP => gpui::ImageFormat::Webp,
558 image::ImageFormat::Gif => gpui::ImageFormat::Gif,
559 image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
560 image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
561 image::ImageFormat::Ico => gpui::ImageFormat::Ico,
562 _ => continue,
563 },
564 content,
565 ));
566 }
567 images
568 })
569 .await,
570 );
571 }
572
573 if images.is_empty() {
574 return;
575 }
576
577 let replacement_text = MentionUri::PastedImage.as_link().to_string();
578 cx.update(|_window, cx| {
579 cx.stop_propagation();
580 })
581 .ok();
582 for image in images {
583 let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
584 editor.update_in(cx, |message_editor, window, cx| {
585 let snapshot = message_editor.snapshot(window, cx);
586 let (excerpt_id, _, buffer_snapshot) =
587 snapshot.buffer_snapshot().as_singleton().unwrap();
588
589 let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
590 let multibuffer_anchor = snapshot
591 .buffer_snapshot()
592 .anchor_in_excerpt(*excerpt_id, text_anchor);
593 message_editor.edit(
594 [(
595 multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
596 format!("{replacement_text} "),
597 )],
598 cx,
599 );
600 (*excerpt_id, text_anchor, multibuffer_anchor)
601 })
602 else {
603 break;
604 };
605
606 let content_len = replacement_text.len();
607 let Some(start_anchor) = multibuffer_anchor else {
608 continue;
609 };
610 let Ok(end_anchor) = editor.update(cx, |editor, cx| {
611 let snapshot = editor.buffer().read(cx).snapshot(cx);
612 snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
613 }) else {
614 continue;
615 };
616 let image = Arc::new(image);
617 let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
618 insert_crease_for_mention(
619 excerpt_id,
620 text_anchor,
621 content_len,
622 MentionUri::PastedImage.name().into(),
623 IconName::Image.path().into(),
624 Some(Task::ready(Ok(image.clone())).shared()),
625 editor.clone(),
626 window,
627 cx,
628 )
629 }) else {
630 continue;
631 };
632 let task = cx
633 .spawn(async move |cx| {
634 let format = image.format;
635 let image = cx
636 .update(|_, cx| LanguageModelImage::from_image(image, cx))
637 .map_err(|e| e.to_string())?
638 .await;
639 drop(tx);
640 if let Some(image) = image {
641 Ok(Mention::Image(MentionImage {
642 data: image.source,
643 format,
644 }))
645 } else {
646 Err("Failed to convert image".into())
647 }
648 })
649 .shared();
650
651 mention_set
652 .update(cx, |mention_set, _cx| {
653 mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
654 })
655 .ok();
656
657 if task.await.notify_async_err(cx).is_none() {
658 editor
659 .update(cx, |editor, cx| {
660 editor.edit([(start_anchor..end_anchor, "")], cx);
661 })
662 .ok();
663 mention_set
664 .update(cx, |mention_set, _cx| {
665 mention_set.remove_mention(&crease_id)
666 })
667 .ok();
668 }
669 }
670 }))
671}
672
673pub(crate) fn insert_crease_for_mention(
674 excerpt_id: ExcerptId,
675 anchor: text::Anchor,
676 content_len: usize,
677 crease_label: SharedString,
678 crease_icon: SharedString,
679 // abs_path: Option<Arc<Path>>,
680 image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
681 editor: Entity<Editor>,
682 window: &mut Window,
683 cx: &mut App,
684) -> Option<(CreaseId, postage::barrier::Sender)> {
685 let (tx, rx) = postage::barrier::channel();
686
687 let crease_id = editor.update(cx, |editor, cx| {
688 let snapshot = editor.buffer().read(cx).snapshot(cx);
689
690 let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
691
692 let start = start.bias_right(&snapshot);
693 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
694
695 let placeholder = FoldPlaceholder {
696 render: render_mention_fold_button(
697 crease_label.clone(),
698 crease_icon.clone(),
699 start..end,
700 rx,
701 image,
702 cx.weak_entity(),
703 cx,
704 ),
705 merge_adjacent: false,
706 ..Default::default()
707 };
708
709 let crease = Crease::Inline {
710 range: start..end,
711 placeholder,
712 render_toggle: None,
713 render_trailer: None,
714 metadata: Some(CreaseMetadata {
715 label: crease_label,
716 icon_path: crease_icon,
717 }),
718 };
719
720 let ids = editor.insert_creases(vec![crease.clone()], cx);
721 editor.fold_creases(vec![crease], false, window, cx);
722
723 Some(ids[0])
724 })?;
725
726 Some((crease_id, tx))
727}
728
729pub(crate) fn crease_for_mention(
730 label: SharedString,
731 icon_path: SharedString,
732 range: Range<Anchor>,
733 editor_entity: WeakEntity<Editor>,
734) -> Crease<Anchor> {
735 let placeholder = FoldPlaceholder {
736 render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
737 merge_adjacent: false,
738 ..Default::default()
739 };
740
741 let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
742
743 Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
744 .with_metadata(CreaseMetadata { icon_path, label })
745}
746
747fn render_fold_icon_button(
748 icon_path: SharedString,
749 label: SharedString,
750 editor: WeakEntity<Editor>,
751) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
752 Arc::new({
753 move |fold_id, fold_range, cx| {
754 let is_in_text_selection = editor
755 .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
756 .unwrap_or_default();
757
758 MentionCrease::new(fold_id, icon_path.clone(), label.clone())
759 .is_toggled(is_in_text_selection)
760 .into_any_element()
761 }
762 })
763}
764
765fn fold_toggle(
766 name: &'static str,
767) -> impl Fn(
768 MultiBufferRow,
769 bool,
770 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
771 &mut Window,
772 &mut App,
773) -> AnyElement {
774 move |row, is_folded, fold, _window, _cx| {
775 Disclosure::new((name, row.0 as u64), !is_folded)
776 .toggle_state(is_folded)
777 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
778 .into_any_element()
779 }
780}
781
782fn full_mention_for_directory(
783 project: &Entity<Project>,
784 abs_path: &Path,
785 cx: &mut App,
786) -> Task<Result<Mention>> {
787 fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
788 let mut files = Vec::new();
789
790 for entry in worktree.child_entries(path) {
791 if entry.is_dir() {
792 files.extend(collect_files_in_path(worktree, &entry.path));
793 } else if entry.is_file() {
794 files.push((
795 entry.path.clone(),
796 worktree
797 .full_path(&entry.path)
798 .to_string_lossy()
799 .to_string(),
800 ));
801 }
802 }
803
804 files
805 }
806
807 let Some(project_path) = project
808 .read(cx)
809 .project_path_for_absolute_path(&abs_path, cx)
810 else {
811 return Task::ready(Err(anyhow!("project path not found")));
812 };
813 let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
814 return Task::ready(Err(anyhow!("project entry not found")));
815 };
816 let directory_path = entry.path.clone();
817 let worktree_id = project_path.worktree_id;
818 let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
819 return Task::ready(Err(anyhow!("worktree not found")));
820 };
821 let project = project.clone();
822 cx.spawn(async move |cx| {
823 let file_paths = worktree.read_with(cx, |worktree, _cx| {
824 collect_files_in_path(worktree, &directory_path)
825 })?;
826 let descendants_future = cx.update(|cx| {
827 futures::future::join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
828 let rel_path = worktree_path
829 .strip_prefix(&directory_path)
830 .log_err()
831 .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
832
833 let open_task = project.update(cx, |project, cx| {
834 project.buffer_store().update(cx, |buffer_store, cx| {
835 let project_path = ProjectPath {
836 worktree_id,
837 path: worktree_path,
838 };
839 buffer_store.open_buffer(project_path, cx)
840 })
841 });
842
843 cx.spawn(async move |cx| {
844 let buffer = open_task.await.log_err()?;
845 let buffer_content = outline::get_buffer_content_or_outline(
846 buffer.clone(),
847 Some(&full_path),
848 &cx,
849 )
850 .await
851 .ok()?;
852
853 Some((rel_path, full_path, buffer_content.text, buffer))
854 })
855 }))
856 })?;
857
858 let contents = cx
859 .background_spawn(async move {
860 let (contents, tracked_buffers) = descendants_future
861 .await
862 .into_iter()
863 .flatten()
864 .map(|(rel_path, full_path, rope, buffer)| {
865 ((rel_path, full_path, rope), buffer)
866 })
867 .unzip();
868 Mention::Text {
869 content: render_directory_contents(contents),
870 tracked_buffers,
871 }
872 })
873 .await;
874 anyhow::Ok(contents)
875 })
876}
877
878fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
879 let mut output = String::new();
880 for (_relative_path, full_path, content) in entries {
881 let fence = codeblock_fence_for_path(Some(&full_path), None);
882 write!(output, "\n{fence}\n{content}\n```").unwrap();
883 }
884 output
885}
886
887fn render_mention_fold_button(
888 label: SharedString,
889 icon: SharedString,
890 range: Range<Anchor>,
891 mut loading_finished: postage::barrier::Receiver,
892 image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
893 editor: WeakEntity<Editor>,
894 cx: &mut App,
895) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
896 let loading = cx.new(|cx| {
897 let loading = cx.spawn(async move |this, cx| {
898 loading_finished.recv().await;
899 this.update(cx, |this: &mut LoadingContext, cx| {
900 this.loading = None;
901 cx.notify();
902 })
903 .ok();
904 });
905 LoadingContext {
906 id: cx.entity_id(),
907 label,
908 icon,
909 range,
910 editor,
911 loading: Some(loading),
912 image: image_task.clone(),
913 }
914 });
915 Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
916}
917
918struct LoadingContext {
919 id: EntityId,
920 label: SharedString,
921 icon: SharedString,
922 range: Range<Anchor>,
923 editor: WeakEntity<Editor>,
924 loading: Option<Task<()>>,
925 image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
926}
927
928impl Render for LoadingContext {
929 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
930 let is_in_text_selection = self
931 .editor
932 .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
933 .unwrap_or_default();
934
935 let id = ElementId::from(("loading_context", self.id));
936
937 MentionCrease::new(id, self.icon.clone(), self.label.clone())
938 .is_toggled(is_in_text_selection)
939 .is_loading(self.loading.is_some())
940 .when_some(self.image.clone(), |this, image_task| {
941 this.image_preview(move |_, cx| {
942 let image = image_task.peek().cloned().transpose().ok().flatten();
943 let image_task = image_task.clone();
944 cx.new::<ImageHover>(|cx| ImageHover {
945 image,
946 _task: cx.spawn(async move |this, cx| {
947 if let Ok(image) = image_task.clone().await {
948 this.update(cx, |this, cx| {
949 if this.image.replace(image).is_none() {
950 cx.notify();
951 }
952 })
953 .ok();
954 }
955 }),
956 })
957 .into()
958 })
959 })
960 }
961}
962
963struct ImageHover {
964 image: Option<Arc<Image>>,
965 _task: Task<()>,
966}
967
968impl Render for ImageHover {
969 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
970 if let Some(image) = self.image.clone() {
971 gpui::img(image).max_w_96().max_h_96().into_any_element()
972 } else {
973 gpui::Empty.into_any_element()
974 }
975 }
976}
977
978async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
979 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
980 enum ContentType {
981 Html,
982 Plaintext,
983 Json,
984 }
985 use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
986
987 let url = if !url.starts_with("https://") && !url.starts_with("http://") {
988 format!("https://{url}")
989 } else {
990 url
991 };
992
993 let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
994 let mut body = Vec::new();
995 response
996 .body_mut()
997 .read_to_end(&mut body)
998 .await
999 .context("error reading response body")?;
1000
1001 if response.status().is_client_error() {
1002 let text = String::from_utf8_lossy(body.as_slice());
1003 anyhow::bail!(
1004 "status error {}, response: {text:?}",
1005 response.status().as_u16()
1006 );
1007 }
1008
1009 let Some(content_type) = response.headers().get("content-type") else {
1010 anyhow::bail!("missing Content-Type header");
1011 };
1012 let content_type = content_type
1013 .to_str()
1014 .context("invalid Content-Type header")?;
1015 let content_type = match content_type {
1016 "text/html" => ContentType::Html,
1017 "text/plain" => ContentType::Plaintext,
1018 "application/json" => ContentType::Json,
1019 _ => ContentType::Html,
1020 };
1021
1022 match content_type {
1023 ContentType::Html => {
1024 let mut handlers: Vec<TagHandler> = vec![
1025 Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
1026 Rc::new(RefCell::new(markdown::ParagraphHandler)),
1027 Rc::new(RefCell::new(markdown::HeadingHandler)),
1028 Rc::new(RefCell::new(markdown::ListHandler)),
1029 Rc::new(RefCell::new(markdown::TableHandler::new())),
1030 Rc::new(RefCell::new(markdown::StyledTextHandler)),
1031 ];
1032 if url.contains("wikipedia.org") {
1033 use html_to_markdown::structure::wikipedia;
1034
1035 handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
1036 handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
1037 handlers.push(Rc::new(
1038 RefCell::new(wikipedia::WikipediaCodeHandler::new()),
1039 ));
1040 } else {
1041 handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
1042 }
1043 convert_html_to_markdown(&body[..], &mut handlers)
1044 }
1045 ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
1046 ContentType::Json => {
1047 let json: serde_json::Value = serde_json::from_slice(&body)?;
1048
1049 Ok(format!(
1050 "```json\n{}\n```",
1051 serde_json::to_string_pretty(&json)?
1052 ))
1053 }
1054 }
1055}