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