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