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
 600/// Inserts a list of images into the editor as context mentions.
 601/// This is the shared implementation used by both paste and file picker operations.
 602pub(crate) async fn insert_images_as_context(
 603    images: Vec<gpui::Image>,
 604    editor: Entity<Editor>,
 605    mention_set: Entity<MentionSet>,
 606    cx: &mut gpui::AsyncWindowContext,
 607) {
 608    if images.is_empty() {
 609        return;
 610    }
 611
 612    let replacement_text = MentionUri::PastedImage.as_link().to_string();
 613
 614    for image in images {
 615        let Some((excerpt_id, text_anchor, multibuffer_anchor)) = editor
 616            .update_in(cx, |editor, window, cx| {
 617                let snapshot = editor.snapshot(window, cx);
 618                let (excerpt_id, _, buffer_snapshot) =
 619                    snapshot.buffer_snapshot().as_singleton().unwrap();
 620
 621                let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 622                let multibuffer_anchor = snapshot
 623                    .buffer_snapshot()
 624                    .anchor_in_excerpt(*excerpt_id, text_anchor);
 625                editor.edit(
 626                    [(
 627                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 628                        format!("{replacement_text} "),
 629                    )],
 630                    cx,
 631                );
 632                (*excerpt_id, text_anchor, multibuffer_anchor)
 633            })
 634            .ok()
 635        else {
 636            break;
 637        };
 638
 639        let content_len = replacement_text.len();
 640        let Some(start_anchor) = multibuffer_anchor else {
 641            continue;
 642        };
 643        let end_anchor = editor.update(cx, |editor, cx| {
 644            let snapshot = editor.buffer().read(cx).snapshot(cx);
 645            snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
 646        });
 647        let image = Arc::new(image);
 648        let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
 649            insert_crease_for_mention(
 650                excerpt_id,
 651                text_anchor,
 652                content_len,
 653                MentionUri::PastedImage.name().into(),
 654                IconName::Image.path().into(),
 655                Some(Task::ready(Ok(image.clone())).shared()),
 656                editor.clone(),
 657                window,
 658                cx,
 659            )
 660        }) else {
 661            continue;
 662        };
 663        let task = cx
 664            .spawn(async move |cx| {
 665                let image = cx
 666                    .update(|_, cx| LanguageModelImage::from_image(image, cx))
 667                    .map_err(|e| e.to_string())?
 668                    .await;
 669                drop(tx);
 670                if let Some(image) = image {
 671                    Ok(Mention::Image(MentionImage {
 672                        data: image.source,
 673                        format: LanguageModelImage::FORMAT,
 674                    }))
 675                } else {
 676                    Err("Failed to convert image".into())
 677                }
 678            })
 679            .shared();
 680
 681        mention_set.update(cx, |mention_set, _cx| {
 682            mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
 683        });
 684
 685        if task.await.notify_async_err(cx).is_none() {
 686            editor.update(cx, |editor, cx| {
 687                editor.edit([(start_anchor..end_anchor, "")], cx);
 688            });
 689            mention_set.update(cx, |mention_set, _cx| {
 690                mention_set.remove_mention(&crease_id)
 691            });
 692        }
 693    }
 694}
 695
 696pub(crate) fn paste_images_as_context(
 697    editor: Entity<Editor>,
 698    mention_set: Entity<MentionSet>,
 699    window: &mut Window,
 700    cx: &mut App,
 701) -> Option<Task<()>> {
 702    let clipboard = cx.read_from_clipboard()?;
 703    Some(window.spawn(cx, async move |cx| {
 704        use itertools::Itertools;
 705        let (mut images, paths) = clipboard
 706            .into_entries()
 707            .filter_map(|entry| match entry {
 708                ClipboardEntry::Image(image) => Some(Either::Left(image)),
 709                ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
 710                _ => None,
 711            })
 712            .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
 713
 714        if !paths.is_empty() {
 715            images.extend(
 716                cx.background_spawn(async move {
 717                    let mut images = vec![];
 718                    for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
 719                        let Ok(content) = async_fs::read(path).await else {
 720                            continue;
 721                        };
 722                        let Ok(format) = image::guess_format(&content) else {
 723                            continue;
 724                        };
 725                        images.push(gpui::Image::from_bytes(
 726                            match format {
 727                                image::ImageFormat::Png => gpui::ImageFormat::Png,
 728                                image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
 729                                image::ImageFormat::WebP => gpui::ImageFormat::Webp,
 730                                image::ImageFormat::Gif => gpui::ImageFormat::Gif,
 731                                image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
 732                                image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
 733                                image::ImageFormat::Ico => gpui::ImageFormat::Ico,
 734                                _ => continue,
 735                            },
 736                            content,
 737                        ));
 738                    }
 739                    images
 740                })
 741                .await,
 742            );
 743        }
 744
 745        cx.update(|_window, cx| {
 746            cx.stop_propagation();
 747        })
 748        .ok();
 749
 750        insert_images_as_context(images, editor, mention_set, cx).await;
 751    }))
 752}
 753
 754pub(crate) fn insert_crease_for_mention(
 755    excerpt_id: ExcerptId,
 756    anchor: text::Anchor,
 757    content_len: usize,
 758    crease_label: SharedString,
 759    crease_icon: SharedString,
 760    // abs_path: Option<Arc<Path>>,
 761    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
 762    editor: Entity<Editor>,
 763    window: &mut Window,
 764    cx: &mut App,
 765) -> Option<(CreaseId, postage::barrier::Sender)> {
 766    let (tx, rx) = postage::barrier::channel();
 767
 768    let crease_id = editor.update(cx, |editor, cx| {
 769        let snapshot = editor.buffer().read(cx).snapshot(cx);
 770
 771        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
 772
 773        let start = start.bias_right(&snapshot);
 774        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
 775
 776        let placeholder = FoldPlaceholder {
 777            render: render_mention_fold_button(
 778                crease_label.clone(),
 779                crease_icon.clone(),
 780                start..end,
 781                rx,
 782                image,
 783                cx.weak_entity(),
 784                cx,
 785            ),
 786            merge_adjacent: false,
 787            ..Default::default()
 788        };
 789
 790        let crease = Crease::Inline {
 791            range: start..end,
 792            placeholder,
 793            render_toggle: None,
 794            render_trailer: None,
 795            metadata: Some(CreaseMetadata {
 796                label: crease_label,
 797                icon_path: crease_icon,
 798            }),
 799        };
 800
 801        let ids = editor.insert_creases(vec![crease.clone()], cx);
 802        editor.fold_creases(vec![crease], false, window, cx);
 803
 804        Some(ids[0])
 805    })?;
 806
 807    Some((crease_id, tx))
 808}
 809
 810pub(crate) fn crease_for_mention(
 811    label: SharedString,
 812    icon_path: SharedString,
 813    range: Range<Anchor>,
 814    editor_entity: WeakEntity<Editor>,
 815) -> Crease<Anchor> {
 816    let placeholder = FoldPlaceholder {
 817        render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
 818        merge_adjacent: false,
 819        ..Default::default()
 820    };
 821
 822    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
 823
 824    Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
 825        .with_metadata(CreaseMetadata { icon_path, label })
 826}
 827
 828fn render_fold_icon_button(
 829    icon_path: SharedString,
 830    label: SharedString,
 831    editor: WeakEntity<Editor>,
 832) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
 833    Arc::new({
 834        move |fold_id, fold_range, cx| {
 835            let is_in_text_selection = editor
 836                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
 837                .unwrap_or_default();
 838
 839            MentionCrease::new(fold_id, icon_path.clone(), label.clone())
 840                .is_toggled(is_in_text_selection)
 841                .into_any_element()
 842        }
 843    })
 844}
 845
 846fn fold_toggle(
 847    name: &'static str,
 848) -> impl Fn(
 849    MultiBufferRow,
 850    bool,
 851    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
 852    &mut Window,
 853    &mut App,
 854) -> AnyElement {
 855    move |row, is_folded, fold, _window, _cx| {
 856        Disclosure::new((name, row.0 as u64), !is_folded)
 857            .toggle_state(is_folded)
 858            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
 859            .into_any_element()
 860    }
 861}
 862
 863fn full_mention_for_directory(
 864    project: &Entity<Project>,
 865    abs_path: &Path,
 866    cx: &mut App,
 867) -> Task<Result<Mention>> {
 868    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
 869        let mut files = Vec::new();
 870
 871        for entry in worktree.child_entries(path) {
 872            if entry.is_dir() {
 873                files.extend(collect_files_in_path(worktree, &entry.path));
 874            } else if entry.is_file() {
 875                files.push((
 876                    entry.path.clone(),
 877                    worktree
 878                        .full_path(&entry.path)
 879                        .to_string_lossy()
 880                        .to_string(),
 881                ));
 882            }
 883        }
 884
 885        files
 886    }
 887
 888    let Some(project_path) = project
 889        .read(cx)
 890        .project_path_for_absolute_path(&abs_path, cx)
 891    else {
 892        return Task::ready(Err(anyhow!("project path not found")));
 893    };
 894    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
 895        return Task::ready(Err(anyhow!("project entry not found")));
 896    };
 897    let directory_path = entry.path.clone();
 898    let worktree_id = project_path.worktree_id;
 899    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
 900        return Task::ready(Err(anyhow!("worktree not found")));
 901    };
 902    let project = project.clone();
 903    cx.spawn(async move |cx| {
 904        let file_paths = worktree.read_with(cx, |worktree, _cx| {
 905            collect_files_in_path(worktree, &directory_path)
 906        });
 907        let descendants_future = cx.update(|cx| {
 908            futures::future::join_all(file_paths.into_iter().map(
 909                |(worktree_path, full_path): (Arc<RelPath>, String)| {
 910                    let rel_path = worktree_path
 911                        .strip_prefix(&directory_path)
 912                        .log_err()
 913                        .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
 914
 915                    let open_task = project.update(cx, |project, cx| {
 916                        project.buffer_store().update(cx, |buffer_store, cx| {
 917                            let project_path = ProjectPath {
 918                                worktree_id,
 919                                path: worktree_path,
 920                            };
 921                            buffer_store.open_buffer(project_path, cx)
 922                        })
 923                    });
 924
 925                    cx.spawn(async move |cx| {
 926                        let buffer = open_task.await.log_err()?;
 927                        let buffer_content = outline::get_buffer_content_or_outline(
 928                            buffer.clone(),
 929                            Some(&full_path),
 930                            &cx,
 931                        )
 932                        .await
 933                        .ok()?;
 934
 935                        Some((rel_path, full_path, buffer_content.text, buffer))
 936                    })
 937                },
 938            ))
 939        });
 940
 941        let contents = cx
 942            .background_spawn(async move {
 943                let (contents, tracked_buffers): (Vec<_>, Vec<_>) = descendants_future
 944                    .await
 945                    .into_iter()
 946                    .flatten()
 947                    .map(|(rel_path, full_path, rope, buffer)| {
 948                        ((rel_path, full_path, rope), buffer)
 949                    })
 950                    .unzip();
 951                Mention::Text {
 952                    content: render_directory_contents(contents),
 953                    tracked_buffers,
 954                }
 955            })
 956            .await;
 957        anyhow::Ok(contents)
 958    })
 959}
 960
 961fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
 962    let mut output = String::new();
 963    for (_relative_path, full_path, content) in entries {
 964        let fence = codeblock_fence_for_path(Some(&full_path), None);
 965        write!(output, "\n{fence}\n{content}\n```").unwrap();
 966    }
 967    output
 968}
 969
 970fn render_mention_fold_button(
 971    label: SharedString,
 972    icon: SharedString,
 973    range: Range<Anchor>,
 974    mut loading_finished: postage::barrier::Receiver,
 975    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
 976    editor: WeakEntity<Editor>,
 977    cx: &mut App,
 978) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
 979    let loading = cx.new(|cx| {
 980        let loading = cx.spawn(async move |this, cx| {
 981            loading_finished.recv().await;
 982            this.update(cx, |this: &mut LoadingContext, cx| {
 983                this.loading = None;
 984                cx.notify();
 985            })
 986            .ok();
 987        });
 988        LoadingContext {
 989            id: cx.entity_id(),
 990            label,
 991            icon,
 992            range,
 993            editor,
 994            loading: Some(loading),
 995            image: image_task.clone(),
 996        }
 997    });
 998    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
 999}
1000
1001struct LoadingContext {
1002    id: EntityId,
1003    label: SharedString,
1004    icon: SharedString,
1005    range: Range<Anchor>,
1006    editor: WeakEntity<Editor>,
1007    loading: Option<Task<()>>,
1008    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1009}
1010
1011impl Render for LoadingContext {
1012    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1013        let is_in_text_selection = self
1014            .editor
1015            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
1016            .unwrap_or_default();
1017
1018        let id = ElementId::from(("loading_context", self.id));
1019
1020        MentionCrease::new(id, self.icon.clone(), self.label.clone())
1021            .is_toggled(is_in_text_selection)
1022            .is_loading(self.loading.is_some())
1023            .when_some(self.image.clone(), |this, image_task| {
1024                this.image_preview(move |_, cx| {
1025                    let image = image_task.peek().cloned().transpose().ok().flatten();
1026                    let image_task = image_task.clone();
1027                    cx.new::<ImageHover>(|cx| ImageHover {
1028                        image,
1029                        _task: cx.spawn(async move |this, cx| {
1030                            if let Ok(image) = image_task.clone().await {
1031                                this.update(cx, |this, cx| {
1032                                    if this.image.replace(image).is_none() {
1033                                        cx.notify();
1034                                    }
1035                                })
1036                                .ok();
1037                            }
1038                        }),
1039                    })
1040                    .into()
1041                })
1042            })
1043    }
1044}
1045
1046struct ImageHover {
1047    image: Option<Arc<Image>>,
1048    _task: Task<()>,
1049}
1050
1051impl Render for ImageHover {
1052    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1053        if let Some(image) = self.image.clone() {
1054            div()
1055                .p_1p5()
1056                .elevation_2(cx)
1057                .child(gpui::img(image).h_auto().max_w_96().rounded_sm())
1058                .into_any_element()
1059        } else {
1060            gpui::Empty.into_any_element()
1061        }
1062    }
1063}
1064
1065async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
1066    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
1067    enum ContentType {
1068        Html,
1069        Plaintext,
1070        Json,
1071    }
1072    use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
1073
1074    let url = if !url.starts_with("https://") && !url.starts_with("http://") {
1075        format!("https://{url}")
1076    } else {
1077        url
1078    };
1079
1080    let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
1081    let mut body = Vec::new();
1082    response
1083        .body_mut()
1084        .read_to_end(&mut body)
1085        .await
1086        .context("error reading response body")?;
1087
1088    if response.status().is_client_error() {
1089        let text = String::from_utf8_lossy(body.as_slice());
1090        anyhow::bail!(
1091            "status error {}, response: {text:?}",
1092            response.status().as_u16()
1093        );
1094    }
1095
1096    let Some(content_type) = response.headers().get("content-type") else {
1097        anyhow::bail!("missing Content-Type header");
1098    };
1099    let content_type = content_type
1100        .to_str()
1101        .context("invalid Content-Type header")?;
1102    let content_type = match content_type {
1103        "text/html" => ContentType::Html,
1104        "text/plain" => ContentType::Plaintext,
1105        "application/json" => ContentType::Json,
1106        _ => ContentType::Html,
1107    };
1108
1109    match content_type {
1110        ContentType::Html => {
1111            let mut handlers: Vec<TagHandler> = vec![
1112                Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
1113                Rc::new(RefCell::new(markdown::ParagraphHandler)),
1114                Rc::new(RefCell::new(markdown::HeadingHandler)),
1115                Rc::new(RefCell::new(markdown::ListHandler)),
1116                Rc::new(RefCell::new(markdown::TableHandler::new())),
1117                Rc::new(RefCell::new(markdown::StyledTextHandler)),
1118            ];
1119            if url.contains("wikipedia.org") {
1120                use html_to_markdown::structure::wikipedia;
1121
1122                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
1123                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
1124                handlers.push(Rc::new(
1125                    RefCell::new(wikipedia::WikipediaCodeHandler::new()),
1126                ));
1127            } else {
1128                handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
1129            }
1130            convert_html_to_markdown(&body[..], &mut handlers)
1131        }
1132        ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
1133        ContentType::Json => {
1134            let json: serde_json::Value = serde_json::from_slice(&body)?;
1135
1136            Ok(format!(
1137                "```json\n{}\n```",
1138                serde_json::to_string_pretty(&json)?
1139            ))
1140        }
1141    }
1142}