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