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