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