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
600pub(crate) fn paste_images_as_context(
601 editor: Entity<Editor>,
602 mention_set: Entity<MentionSet>,
603 window: &mut Window,
604 cx: &mut App,
605) -> Option<Task<()>> {
606 let clipboard = cx.read_from_clipboard()?;
607 Some(window.spawn(cx, async move |cx| {
608 use itertools::Itertools;
609 let (mut images, paths) = clipboard
610 .into_entries()
611 .filter_map(|entry| match entry {
612 ClipboardEntry::Image(image) => Some(Either::Left(image)),
613 ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
614 _ => None,
615 })
616 .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
617
618 if !paths.is_empty() {
619 images.extend(
620 cx.background_spawn(async move {
621 let mut images = vec![];
622 for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
623 let Ok(content) = async_fs::read(path).await else {
624 continue;
625 };
626 let Ok(format) = image::guess_format(&content) else {
627 continue;
628 };
629 images.push(gpui::Image::from_bytes(
630 match format {
631 image::ImageFormat::Png => gpui::ImageFormat::Png,
632 image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
633 image::ImageFormat::WebP => gpui::ImageFormat::Webp,
634 image::ImageFormat::Gif => gpui::ImageFormat::Gif,
635 image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
636 image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
637 image::ImageFormat::Ico => gpui::ImageFormat::Ico,
638 _ => continue,
639 },
640 content,
641 ));
642 }
643 images
644 })
645 .await,
646 );
647 }
648
649 if images.is_empty() {
650 return;
651 }
652
653 let replacement_text = MentionUri::PastedImage.as_link().to_string();
654 cx.update(|_window, cx| {
655 cx.stop_propagation();
656 })
657 .ok();
658 for image in images {
659 let Some((excerpt_id, text_anchor, multibuffer_anchor)) = editor
660 .update_in(cx, |message_editor, window, cx| {
661 let snapshot = message_editor.snapshot(window, cx);
662 let (excerpt_id, _, buffer_snapshot) =
663 snapshot.buffer_snapshot().as_singleton().unwrap();
664
665 let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
666 let multibuffer_anchor = snapshot
667 .buffer_snapshot()
668 .anchor_in_excerpt(*excerpt_id, text_anchor);
669 message_editor.edit(
670 [(
671 multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
672 format!("{replacement_text} "),
673 )],
674 cx,
675 );
676 (*excerpt_id, text_anchor, multibuffer_anchor)
677 })
678 .ok()
679 else {
680 break;
681 };
682
683 let content_len = replacement_text.len();
684 let Some(start_anchor) = multibuffer_anchor else {
685 continue;
686 };
687 let end_anchor = editor.update(cx, |editor, cx| {
688 let snapshot = editor.buffer().read(cx).snapshot(cx);
689 snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
690 });
691 let image = Arc::new(image);
692 let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
693 insert_crease_for_mention(
694 excerpt_id,
695 text_anchor,
696 content_len,
697 MentionUri::PastedImage.name().into(),
698 IconName::Image.path().into(),
699 Some(Task::ready(Ok(image.clone())).shared()),
700 editor.clone(),
701 window,
702 cx,
703 )
704 }) else {
705 continue;
706 };
707 let task = cx
708 .spawn(async move |cx| {
709 let image = cx
710 .update(|_, cx| LanguageModelImage::from_image(image, cx))
711 .map_err(|e| e.to_string())?
712 .await;
713 drop(tx);
714 if let Some(image) = image {
715 Ok(Mention::Image(MentionImage {
716 data: image.source,
717 format: LanguageModelImage::FORMAT,
718 }))
719 } else {
720 Err("Failed to convert image".into())
721 }
722 })
723 .shared();
724
725 mention_set.update(cx, |mention_set, _cx| {
726 mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
727 });
728
729 if task.await.notify_async_err(cx).is_none() {
730 editor.update(cx, |editor, cx| {
731 editor.edit([(start_anchor..end_anchor, "")], cx);
732 });
733 mention_set.update(cx, |mention_set, _cx| {
734 mention_set.remove_mention(&crease_id)
735 });
736 }
737 }
738 }))
739}
740
741pub(crate) fn insert_crease_for_mention(
742 excerpt_id: ExcerptId,
743 anchor: text::Anchor,
744 content_len: usize,
745 crease_label: SharedString,
746 crease_icon: SharedString,
747 // abs_path: Option<Arc<Path>>,
748 image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
749 editor: Entity<Editor>,
750 window: &mut Window,
751 cx: &mut App,
752) -> Option<(CreaseId, postage::barrier::Sender)> {
753 let (tx, rx) = postage::barrier::channel();
754
755 let crease_id = editor.update(cx, |editor, cx| {
756 let snapshot = editor.buffer().read(cx).snapshot(cx);
757
758 let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
759
760 let start = start.bias_right(&snapshot);
761 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
762
763 let placeholder = FoldPlaceholder {
764 render: render_mention_fold_button(
765 crease_label.clone(),
766 crease_icon.clone(),
767 start..end,
768 rx,
769 image,
770 cx.weak_entity(),
771 cx,
772 ),
773 merge_adjacent: false,
774 ..Default::default()
775 };
776
777 let crease = Crease::Inline {
778 range: start..end,
779 placeholder,
780 render_toggle: None,
781 render_trailer: None,
782 metadata: Some(CreaseMetadata {
783 label: crease_label,
784 icon_path: crease_icon,
785 }),
786 };
787
788 let ids = editor.insert_creases(vec![crease.clone()], cx);
789 editor.fold_creases(vec![crease], false, window, cx);
790
791 Some(ids[0])
792 })?;
793
794 Some((crease_id, tx))
795}
796
797pub(crate) fn crease_for_mention(
798 label: SharedString,
799 icon_path: SharedString,
800 range: Range<Anchor>,
801 editor_entity: WeakEntity<Editor>,
802) -> Crease<Anchor> {
803 let placeholder = FoldPlaceholder {
804 render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
805 merge_adjacent: false,
806 ..Default::default()
807 };
808
809 let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
810
811 Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
812 .with_metadata(CreaseMetadata { icon_path, label })
813}
814
815fn render_fold_icon_button(
816 icon_path: SharedString,
817 label: SharedString,
818 editor: WeakEntity<Editor>,
819) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
820 Arc::new({
821 move |fold_id, fold_range, cx| {
822 let is_in_text_selection = editor
823 .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
824 .unwrap_or_default();
825
826 MentionCrease::new(fold_id, icon_path.clone(), label.clone())
827 .is_toggled(is_in_text_selection)
828 .into_any_element()
829 }
830 })
831}
832
833fn fold_toggle(
834 name: &'static str,
835) -> impl Fn(
836 MultiBufferRow,
837 bool,
838 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
839 &mut Window,
840 &mut App,
841) -> AnyElement {
842 move |row, is_folded, fold, _window, _cx| {
843 Disclosure::new((name, row.0 as u64), !is_folded)
844 .toggle_state(is_folded)
845 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
846 .into_any_element()
847 }
848}
849
850fn full_mention_for_directory(
851 project: &Entity<Project>,
852 abs_path: &Path,
853 cx: &mut App,
854) -> Task<Result<Mention>> {
855 fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
856 let mut files = Vec::new();
857
858 for entry in worktree.child_entries(path) {
859 if entry.is_dir() {
860 files.extend(collect_files_in_path(worktree, &entry.path));
861 } else if entry.is_file() {
862 files.push((
863 entry.path.clone(),
864 worktree
865 .full_path(&entry.path)
866 .to_string_lossy()
867 .to_string(),
868 ));
869 }
870 }
871
872 files
873 }
874
875 let Some(project_path) = project
876 .read(cx)
877 .project_path_for_absolute_path(&abs_path, cx)
878 else {
879 return Task::ready(Err(anyhow!("project path not found")));
880 };
881 let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
882 return Task::ready(Err(anyhow!("project entry not found")));
883 };
884 let directory_path = entry.path.clone();
885 let worktree_id = project_path.worktree_id;
886 let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
887 return Task::ready(Err(anyhow!("worktree not found")));
888 };
889 let project = project.clone();
890 cx.spawn(async move |cx| {
891 let file_paths = worktree.read_with(cx, |worktree, _cx| {
892 collect_files_in_path(worktree, &directory_path)
893 });
894 let descendants_future = cx.update(|cx| {
895 futures::future::join_all(file_paths.into_iter().map(
896 |(worktree_path, full_path): (Arc<RelPath>, String)| {
897 let rel_path = worktree_path
898 .strip_prefix(&directory_path)
899 .log_err()
900 .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
901
902 let open_task = project.update(cx, |project, cx| {
903 project.buffer_store().update(cx, |buffer_store, cx| {
904 let project_path = ProjectPath {
905 worktree_id,
906 path: worktree_path,
907 };
908 buffer_store.open_buffer(project_path, cx)
909 })
910 });
911
912 cx.spawn(async move |cx| {
913 let buffer = open_task.await.log_err()?;
914 let buffer_content = outline::get_buffer_content_or_outline(
915 buffer.clone(),
916 Some(&full_path),
917 &cx,
918 )
919 .await
920 .ok()?;
921
922 Some((rel_path, full_path, buffer_content.text, buffer))
923 })
924 },
925 ))
926 });
927
928 let contents = cx
929 .background_spawn(async move {
930 let (contents, tracked_buffers): (Vec<_>, Vec<_>) = descendants_future
931 .await
932 .into_iter()
933 .flatten()
934 .map(|(rel_path, full_path, rope, buffer)| {
935 ((rel_path, full_path, rope), buffer)
936 })
937 .unzip();
938 Mention::Text {
939 content: render_directory_contents(contents),
940 tracked_buffers,
941 }
942 })
943 .await;
944 anyhow::Ok(contents)
945 })
946}
947
948fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
949 let mut output = String::new();
950 for (_relative_path, full_path, content) in entries {
951 let fence = codeblock_fence_for_path(Some(&full_path), None);
952 write!(output, "\n{fence}\n{content}\n```").unwrap();
953 }
954 output
955}
956
957fn render_mention_fold_button(
958 label: SharedString,
959 icon: SharedString,
960 range: Range<Anchor>,
961 mut loading_finished: postage::barrier::Receiver,
962 image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
963 editor: WeakEntity<Editor>,
964 cx: &mut App,
965) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
966 let loading = cx.new(|cx| {
967 let loading = cx.spawn(async move |this, cx| {
968 loading_finished.recv().await;
969 this.update(cx, |this: &mut LoadingContext, cx| {
970 this.loading = None;
971 cx.notify();
972 })
973 .ok();
974 });
975 LoadingContext {
976 id: cx.entity_id(),
977 label,
978 icon,
979 range,
980 editor,
981 loading: Some(loading),
982 image: image_task.clone(),
983 }
984 });
985 Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
986}
987
988struct LoadingContext {
989 id: EntityId,
990 label: SharedString,
991 icon: SharedString,
992 range: Range<Anchor>,
993 editor: WeakEntity<Editor>,
994 loading: Option<Task<()>>,
995 image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
996}
997
998impl Render for LoadingContext {
999 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1000 let is_in_text_selection = self
1001 .editor
1002 .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
1003 .unwrap_or_default();
1004
1005 let id = ElementId::from(("loading_context", self.id));
1006
1007 MentionCrease::new(id, self.icon.clone(), self.label.clone())
1008 .is_toggled(is_in_text_selection)
1009 .is_loading(self.loading.is_some())
1010 .when_some(self.image.clone(), |this, image_task| {
1011 this.image_preview(move |_, cx| {
1012 let image = image_task.peek().cloned().transpose().ok().flatten();
1013 let image_task = image_task.clone();
1014 cx.new::<ImageHover>(|cx| ImageHover {
1015 image,
1016 _task: cx.spawn(async move |this, cx| {
1017 if let Ok(image) = image_task.clone().await {
1018 this.update(cx, |this, cx| {
1019 if this.image.replace(image).is_none() {
1020 cx.notify();
1021 }
1022 })
1023 .ok();
1024 }
1025 }),
1026 })
1027 .into()
1028 })
1029 })
1030 }
1031}
1032
1033struct ImageHover {
1034 image: Option<Arc<Image>>,
1035 _task: Task<()>,
1036}
1037
1038impl Render for ImageHover {
1039 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1040 if let Some(image) = self.image.clone() {
1041 gpui::img(image).max_w_96().max_h_96().into_any_element()
1042 } else {
1043 gpui::Empty.into_any_element()
1044 }
1045 }
1046}
1047
1048async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
1049 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
1050 enum ContentType {
1051 Html,
1052 Plaintext,
1053 Json,
1054 }
1055 use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
1056
1057 let url = if !url.starts_with("https://") && !url.starts_with("http://") {
1058 format!("https://{url}")
1059 } else {
1060 url
1061 };
1062
1063 let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
1064 let mut body = Vec::new();
1065 response
1066 .body_mut()
1067 .read_to_end(&mut body)
1068 .await
1069 .context("error reading response body")?;
1070
1071 if response.status().is_client_error() {
1072 let text = String::from_utf8_lossy(body.as_slice());
1073 anyhow::bail!(
1074 "status error {}, response: {text:?}",
1075 response.status().as_u16()
1076 );
1077 }
1078
1079 let Some(content_type) = response.headers().get("content-type") else {
1080 anyhow::bail!("missing Content-Type header");
1081 };
1082 let content_type = content_type
1083 .to_str()
1084 .context("invalid Content-Type header")?;
1085 let content_type = match content_type {
1086 "text/html" => ContentType::Html,
1087 "text/plain" => ContentType::Plaintext,
1088 "application/json" => ContentType::Json,
1089 _ => ContentType::Html,
1090 };
1091
1092 match content_type {
1093 ContentType::Html => {
1094 let mut handlers: Vec<TagHandler> = vec![
1095 Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
1096 Rc::new(RefCell::new(markdown::ParagraphHandler)),
1097 Rc::new(RefCell::new(markdown::HeadingHandler)),
1098 Rc::new(RefCell::new(markdown::ListHandler)),
1099 Rc::new(RefCell::new(markdown::TableHandler::new())),
1100 Rc::new(RefCell::new(markdown::StyledTextHandler)),
1101 ];
1102 if url.contains("wikipedia.org") {
1103 use html_to_markdown::structure::wikipedia;
1104
1105 handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
1106 handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
1107 handlers.push(Rc::new(
1108 RefCell::new(wikipedia::WikipediaCodeHandler::new()),
1109 ));
1110 } else {
1111 handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
1112 }
1113 convert_html_to_markdown(&body[..], &mut handlers)
1114 }
1115 ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
1116 ContentType::Json => {
1117 let json: serde_json::Value = serde_json::from_slice(&body)?;
1118
1119 Ok(format!(
1120 "```json\n{}\n```",
1121 serde_json::to_string_pretty(&json)?
1122 ))
1123 }
1124 }
1125}