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