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