mention_set.rs

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