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