mention_set.rs

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