mention_set.rs

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