mention_set.rs

   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}