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