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