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