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