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, EditorEvent, 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    Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
  16    Image, ImageFormat, Img, SharedString, Subscription, Task, WeakEntity, pulsating_between,
  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    time::Duration,
  36};
  37use text::OffsetRangeExt;
  38use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
  39use util::{ResultExt, debug_panic, rel_path::RelPath};
  40use workspace::{Workspace, notifications::NotifyResultExt as _};
  41
  42pub type MentionTask = Shared<Task<Result<Mention, String>>>;
  43
  44#[derive(Debug, Clone, Eq, PartialEq)]
  45pub enum Mention {
  46    Text {
  47        content: String,
  48        tracked_buffers: Vec<Entity<Buffer>>,
  49    },
  50    Image(MentionImage),
  51    Link,
  52}
  53
  54#[derive(Clone, Debug, Eq, PartialEq)]
  55pub struct MentionImage {
  56    pub data: SharedString,
  57    pub format: ImageFormat,
  58}
  59
  60pub struct MentionSet {
  61    editor: Entity<Editor>,
  62    project: WeakEntity<Project>,
  63    history_store: Entity<HistoryStore>,
  64    prompt_store: Option<Entity<PromptStore>>,
  65    mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
  66    _editor_subscription: Subscription,
  67}
  68
  69impl MentionSet {
  70    pub fn new(
  71        editor: Entity<Editor>,
  72        project: WeakEntity<Project>,
  73        history_store: Entity<HistoryStore>,
  74        prompt_store: Option<Entity<PromptStore>>,
  75        window: &mut Window,
  76        cx: &mut Context<Self>,
  77    ) -> Self {
  78        let editor_subscription =
  79            cx.subscribe_in(&editor, window, move |this, editor, event, window, cx| {
  80                if let EditorEvent::Edited { .. } = event
  81                    && !editor.read(cx).read_only(cx)
  82                {
  83                    let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
  84                    this.remove_invalid(snapshot);
  85                }
  86            });
  87
  88        Self {
  89            editor,
  90            project,
  91            history_store,
  92            prompt_store,
  93            mentions: HashMap::default(),
  94            _editor_subscription: editor_subscription,
  95        }
  96    }
  97
  98    pub fn contents(
  99        &self,
 100        full_mention_content: bool,
 101        cx: &mut App,
 102    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
 103        let Some(project) = self.project.upgrade() else {
 104            return Task::ready(Err(anyhow!("Project not found")));
 105        };
 106        let mentions = self.mentions.clone();
 107        cx.spawn(async move |cx| {
 108            let mut contents = HashMap::default();
 109            for (crease_id, (mention_uri, task)) in mentions {
 110                let content = if full_mention_content
 111                    && let MentionUri::Directory { abs_path } = &mention_uri
 112                {
 113                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
 114                        .await?
 115                } else {
 116                    task.await.map_err(|e| anyhow!("{e}"))?
 117                };
 118
 119                contents.insert(crease_id, (mention_uri, content));
 120            }
 121            Ok(contents)
 122        })
 123    }
 124
 125    fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
 126        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 127            if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
 128                self.mentions.remove(&crease_id);
 129            }
 130        }
 131    }
 132
 133    pub fn insert_mention(&mut self, crease_id: CreaseId, uri: MentionUri, task: MentionTask) {
 134        self.mentions.insert(crease_id, (uri, task));
 135    }
 136
 137    pub fn remove_mention(&mut self, crease_id: &CreaseId) {
 138        self.mentions.remove(crease_id);
 139    }
 140
 141    pub fn creases(&self) -> HashSet<CreaseId> {
 142        self.mentions.keys().cloned().collect()
 143    }
 144
 145    pub fn mentions(&self) -> HashSet<MentionUri> {
 146        self.mentions.values().map(|(uri, _)| uri.clone()).collect()
 147    }
 148
 149    pub fn remove_all(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
 150        self.mentions.drain()
 151    }
 152
 153    pub fn confirm_mention_completion(
 154        &mut self,
 155        crease_text: SharedString,
 156        start: text::Anchor,
 157        content_len: usize,
 158        mention_uri: MentionUri,
 159        supports_images: bool,
 160        workspace: &Entity<Workspace>,
 161        window: &mut Window,
 162        cx: &mut Context<Self>,
 163    ) -> Task<()> {
 164        let Some(project) = self.project.upgrade() else {
 165            return Task::ready(());
 166        };
 167
 168        let snapshot = self
 169            .editor
 170            .update(cx, |editor, cx| editor.snapshot(window, cx));
 171        let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
 172            return Task::ready(());
 173        };
 174        let excerpt_id = start_anchor.excerpt_id;
 175        let end_anchor = snapshot.buffer_snapshot().anchor_before(
 176            start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
 177        );
 178
 179        let crease = if let MentionUri::File { abs_path } = &mention_uri
 180            && let Some(extension) = abs_path.extension()
 181            && let Some(extension) = extension.to_str()
 182            && Img::extensions().contains(&extension)
 183            && !extension.contains("svg")
 184        {
 185            let Some(project_path) = project
 186                .read(cx)
 187                .project_path_for_absolute_path(&abs_path, cx)
 188            else {
 189                log::error!("project path not found");
 190                return Task::ready(());
 191            };
 192            let image_task = project.update(cx, |project, cx| project.open_image(project_path, cx));
 193            let image = cx
 194                .spawn(async move |_, cx| {
 195                    let image = image_task.await.map_err(|e| e.to_string())?;
 196                    let image = image
 197                        .update(cx, |image, _| image.image.clone())
 198                        .map_err(|e| e.to_string())?;
 199                    Ok(image)
 200                })
 201                .shared();
 202            insert_crease_for_mention(
 203                excerpt_id,
 204                start,
 205                content_len,
 206                mention_uri.name().into(),
 207                IconName::Image.path().into(),
 208                Some(image),
 209                self.editor.clone(),
 210                window,
 211                cx,
 212            )
 213        } else {
 214            insert_crease_for_mention(
 215                excerpt_id,
 216                start,
 217                content_len,
 218                crease_text,
 219                mention_uri.icon_path(cx),
 220                None,
 221                self.editor.clone(),
 222                window,
 223                cx,
 224            )
 225        };
 226        let Some((crease_id, tx)) = crease else {
 227            return Task::ready(());
 228        };
 229
 230        let task = match mention_uri.clone() {
 231            MentionUri::Fetch { url } => {
 232                self.confirm_mention_for_fetch(url, workspace.read(cx).client().http_client(), cx)
 233            }
 234            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
 235            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
 236            MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
 237            MentionUri::File { abs_path } => {
 238                self.confirm_mention_for_file(abs_path, supports_images, cx)
 239            }
 240            MentionUri::Symbol {
 241                abs_path,
 242                line_range,
 243                ..
 244            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
 245            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
 246            MentionUri::PastedImage => {
 247                debug_panic!("pasted image URI should not be included in completions");
 248                Task::ready(Err(anyhow!(
 249                    "pasted imaged URI should not be included in completions"
 250                )))
 251            }
 252            MentionUri::Selection { .. } => {
 253                debug_panic!("unexpected selection URI");
 254                Task::ready(Err(anyhow!("unexpected selection 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                    this.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 format = image.format;
 309                let image = cx
 310                    .update(|cx| LanguageModelImage::from_image(image, cx))?
 311                    .await;
 312                if let Some(image) = image {
 313                    Ok(Mention::Image(MentionImage {
 314                        data: image.source,
 315                        format,
 316                    }))
 317                } else {
 318                    Err(anyhow!("Failed to convert image"))
 319                }
 320            });
 321        }
 322
 323        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
 324        cx.spawn(async move |_, cx| {
 325            let buffer = buffer.await?;
 326            let buffer_content = outline::get_buffer_content_or_outline(
 327                buffer.clone(),
 328                Some(&abs_path.to_string_lossy()),
 329                &cx,
 330            )
 331            .await?;
 332
 333            Ok(Mention::Text {
 334                content: buffer_content.text,
 335                tracked_buffers: vec![buffer],
 336            })
 337        })
 338    }
 339
 340    fn confirm_mention_for_fetch(
 341        &self,
 342        url: url::Url,
 343        http_client: Arc<HttpClientWithUrl>,
 344        cx: &mut Context<Self>,
 345    ) -> Task<Result<Mention>> {
 346        cx.background_executor().spawn(async move {
 347            let content = fetch_url_content(http_client, url.to_string()).await?;
 348            Ok(Mention::Text {
 349                content,
 350                tracked_buffers: Vec::new(),
 351            })
 352        })
 353    }
 354
 355    fn confirm_mention_for_symbol(
 356        &self,
 357        abs_path: PathBuf,
 358        line_range: RangeInclusive<u32>,
 359        cx: &mut Context<Self>,
 360    ) -> Task<Result<Mention>> {
 361        let Some(project) = self.project.upgrade() else {
 362            return Task::ready(Err(anyhow!("project not found")));
 363        };
 364        let Some(project_path) = project
 365            .read(cx)
 366            .project_path_for_absolute_path(&abs_path, cx)
 367        else {
 368            return Task::ready(Err(anyhow!("project path not found")));
 369        };
 370        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
 371        cx.spawn(async move |_, cx| {
 372            let buffer = buffer.await?;
 373            let mention = buffer.update(cx, |buffer, cx| {
 374                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
 375                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
 376                let content = buffer.text_for_range(start..end).collect();
 377                Mention::Text {
 378                    content,
 379                    tracked_buffers: vec![cx.entity()],
 380                }
 381            })?;
 382            anyhow::Ok(mention)
 383        })
 384    }
 385
 386    fn confirm_mention_for_rule(
 387        &mut self,
 388        id: PromptId,
 389        cx: &mut Context<Self>,
 390    ) -> Task<Result<Mention>> {
 391        let Some(prompt_store) = self.prompt_store.as_ref() else {
 392            return Task::ready(Err(anyhow!("Missing prompt store")));
 393        };
 394        let prompt = prompt_store.read(cx).load(id, cx);
 395        cx.spawn(async move |_, _| {
 396            let prompt = prompt.await?;
 397            Ok(Mention::Text {
 398                content: prompt,
 399                tracked_buffers: Vec::new(),
 400            })
 401        })
 402    }
 403
 404    pub fn confirm_mention_for_selection(
 405        &mut self,
 406        source_range: Range<text::Anchor>,
 407        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
 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 = self.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                self.editor.downgrade(),
 447            );
 448
 449            let crease_id = self.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        let editor = self.editor.clone();
 475        cx.on_next_frame(window, move |_, window, cx| {
 476            cx.on_next_frame(window, move |_, _, cx| {
 477                editor.update(cx, |editor, cx| {
 478                    editor.request_autoscroll(Autoscroll::fit(), cx)
 479                });
 480            });
 481        });
 482    }
 483
 484    fn confirm_mention_for_thread(
 485        &mut self,
 486        id: acp::SessionId,
 487        cx: &mut Context<Self>,
 488    ) -> Task<Result<Mention>> {
 489        let Some(project) = self.project.upgrade() else {
 490            return Task::ready(Err(anyhow!("project not found")));
 491        };
 492
 493        let server = Rc::new(agent::NativeAgentServer::new(
 494            project.read(cx).fs().clone(),
 495            self.history_store.clone(),
 496        ));
 497        let delegate = AgentServerDelegate::new(
 498            project.read(cx).agent_server_store().clone(),
 499            project.clone(),
 500            None,
 501            None,
 502        );
 503        let connection = server.connect(None, delegate, cx);
 504        cx.spawn(async move |_, cx| {
 505            let (agent, _) = connection.await?;
 506            let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
 507            let summary = agent
 508                .0
 509                .update(cx, |agent, cx| agent.thread_summary(id, cx))?
 510                .await?;
 511            anyhow::Ok(Mention::Text {
 512                content: summary.to_string(),
 513                tracked_buffers: Vec::new(),
 514            })
 515        })
 516    }
 517
 518    fn confirm_mention_for_text_thread(
 519        &mut self,
 520        path: PathBuf,
 521        cx: &mut Context<Self>,
 522    ) -> Task<Result<Mention>> {
 523        let text_thread_task = self.history_store.update(cx, |store, cx| {
 524            store.load_text_thread(path.as_path().into(), cx)
 525        });
 526        cx.spawn(async move |_, cx| {
 527            let text_thread = text_thread_task.await?;
 528            let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
 529            Ok(Mention::Text {
 530                content: xml,
 531                tracked_buffers: Vec::new(),
 532            })
 533        })
 534    }
 535}
 536
 537pub(crate) fn paste_images_as_context(
 538    editor: Entity<Editor>,
 539    mention_set: Entity<MentionSet>,
 540    window: &mut Window,
 541    cx: &mut App,
 542) -> Option<Task<()>> {
 543    let clipboard = cx.read_from_clipboard()?;
 544    Some(window.spawn(cx, async move |cx| {
 545        use itertools::Itertools;
 546        let (mut images, paths) = clipboard
 547            .into_entries()
 548            .filter_map(|entry| match entry {
 549                ClipboardEntry::Image(image) => Some(Either::Left(image)),
 550                ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
 551                _ => None,
 552            })
 553            .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
 554
 555        if !paths.is_empty() {
 556            images.extend(
 557                cx.background_spawn(async move {
 558                    let mut images = vec![];
 559                    for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
 560                        let Ok(content) = async_fs::read(path).await else {
 561                            continue;
 562                        };
 563                        let Ok(format) = image::guess_format(&content) else {
 564                            continue;
 565                        };
 566                        images.push(gpui::Image::from_bytes(
 567                            match format {
 568                                image::ImageFormat::Png => gpui::ImageFormat::Png,
 569                                image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
 570                                image::ImageFormat::WebP => gpui::ImageFormat::Webp,
 571                                image::ImageFormat::Gif => gpui::ImageFormat::Gif,
 572                                image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
 573                                image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
 574                                image::ImageFormat::Ico => gpui::ImageFormat::Ico,
 575                                _ => continue,
 576                            },
 577                            content,
 578                        ));
 579                    }
 580                    images
 581                })
 582                .await,
 583            );
 584        }
 585
 586        if images.is_empty() {
 587            return;
 588        }
 589
 590        let replacement_text = MentionUri::PastedImage.as_link().to_string();
 591        cx.update(|_window, cx| {
 592            cx.stop_propagation();
 593        })
 594        .ok();
 595        for image in images {
 596            let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
 597                editor.update_in(cx, |message_editor, window, cx| {
 598                    let snapshot = message_editor.snapshot(window, cx);
 599                    let (excerpt_id, _, buffer_snapshot) =
 600                        snapshot.buffer_snapshot().as_singleton().unwrap();
 601
 602                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 603                    let multibuffer_anchor = snapshot
 604                        .buffer_snapshot()
 605                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 606                    message_editor.edit(
 607                        [(
 608                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 609                            format!("{replacement_text} "),
 610                        )],
 611                        cx,
 612                    );
 613                    (*excerpt_id, text_anchor, multibuffer_anchor)
 614                })
 615            else {
 616                break;
 617            };
 618
 619            let content_len = replacement_text.len();
 620            let Some(start_anchor) = multibuffer_anchor else {
 621                continue;
 622            };
 623            let Ok(end_anchor) = editor.update(cx, |editor, cx| {
 624                let snapshot = editor.buffer().read(cx).snapshot(cx);
 625                snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
 626            }) else {
 627                continue;
 628            };
 629            let image = Arc::new(image);
 630            let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
 631                insert_crease_for_mention(
 632                    excerpt_id,
 633                    text_anchor,
 634                    content_len,
 635                    MentionUri::PastedImage.name().into(),
 636                    IconName::Image.path().into(),
 637                    Some(Task::ready(Ok(image.clone())).shared()),
 638                    editor.clone(),
 639                    window,
 640                    cx,
 641                )
 642            }) else {
 643                continue;
 644            };
 645            let task = cx
 646                .spawn(async move |cx| {
 647                    let format = image.format;
 648                    let image = cx
 649                        .update(|_, cx| LanguageModelImage::from_image(image, cx))
 650                        .map_err(|e| e.to_string())?
 651                        .await;
 652                    drop(tx);
 653                    if let Some(image) = image {
 654                        Ok(Mention::Image(MentionImage {
 655                            data: image.source,
 656                            format,
 657                        }))
 658                    } else {
 659                        Err("Failed to convert image".into())
 660                    }
 661                })
 662                .shared();
 663
 664            mention_set
 665                .update(cx, |mention_set, _cx| {
 666                    mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
 667                })
 668                .ok();
 669
 670            if task.await.notify_async_err(cx).is_none() {
 671                editor
 672                    .update(cx, |editor, cx| {
 673                        editor.edit([(start_anchor..end_anchor, "")], cx);
 674                    })
 675                    .ok();
 676                mention_set
 677                    .update(cx, |mention_set, _cx| {
 678                        mention_set.remove_mention(&crease_id)
 679                    })
 680                    .ok();
 681            }
 682        }
 683    }))
 684}
 685
 686pub(crate) fn insert_crease_for_mention(
 687    excerpt_id: ExcerptId,
 688    anchor: text::Anchor,
 689    content_len: usize,
 690    crease_label: SharedString,
 691    crease_icon: SharedString,
 692    // abs_path: Option<Arc<Path>>,
 693    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
 694    editor: Entity<Editor>,
 695    window: &mut Window,
 696    cx: &mut App,
 697) -> Option<(CreaseId, postage::barrier::Sender)> {
 698    let (tx, rx) = postage::barrier::channel();
 699
 700    let crease_id = editor.update(cx, |editor, cx| {
 701        let snapshot = editor.buffer().read(cx).snapshot(cx);
 702
 703        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
 704
 705        let start = start.bias_right(&snapshot);
 706        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
 707
 708        let placeholder = FoldPlaceholder {
 709            render: render_mention_fold_button(
 710                crease_label.clone(),
 711                crease_icon.clone(),
 712                start..end,
 713                rx,
 714                image,
 715                cx.weak_entity(),
 716                cx,
 717            ),
 718            merge_adjacent: false,
 719            ..Default::default()
 720        };
 721
 722        let crease = Crease::Inline {
 723            range: start..end,
 724            placeholder,
 725            render_toggle: None,
 726            render_trailer: None,
 727            metadata: Some(CreaseMetadata {
 728                label: crease_label,
 729                icon_path: crease_icon,
 730            }),
 731        };
 732
 733        let ids = editor.insert_creases(vec![crease.clone()], cx);
 734        editor.fold_creases(vec![crease], false, window, cx);
 735
 736        Some(ids[0])
 737    })?;
 738
 739    Some((crease_id, tx))
 740}
 741
 742pub(crate) fn crease_for_mention(
 743    label: SharedString,
 744    icon_path: SharedString,
 745    range: Range<Anchor>,
 746    editor_entity: WeakEntity<Editor>,
 747) -> Crease<Anchor> {
 748    let placeholder = FoldPlaceholder {
 749        render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
 750        merge_adjacent: false,
 751        ..Default::default()
 752    };
 753
 754    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
 755
 756    Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
 757        .with_metadata(CreaseMetadata { icon_path, label })
 758}
 759
 760fn render_fold_icon_button(
 761    icon_path: SharedString,
 762    label: SharedString,
 763    editor: WeakEntity<Editor>,
 764) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
 765    Arc::new({
 766        move |fold_id, fold_range, cx| {
 767            let is_in_text_selection = editor
 768                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
 769                .unwrap_or_default();
 770
 771            ButtonLike::new(fold_id)
 772                .style(ButtonStyle::Filled)
 773                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 774                .toggle_state(is_in_text_selection)
 775                .child(
 776                    h_flex()
 777                        .gap_1()
 778                        .child(
 779                            Icon::from_path(icon_path.clone())
 780                                .size(IconSize::XSmall)
 781                                .color(Color::Muted),
 782                        )
 783                        .child(
 784                            Label::new(label.clone())
 785                                .size(LabelSize::Small)
 786                                .buffer_font(cx)
 787                                .single_line(),
 788                        ),
 789                )
 790                .into_any_element()
 791        }
 792    })
 793}
 794
 795fn fold_toggle(
 796    name: &'static str,
 797) -> impl Fn(
 798    MultiBufferRow,
 799    bool,
 800    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
 801    &mut Window,
 802    &mut App,
 803) -> AnyElement {
 804    move |row, is_folded, fold, _window, _cx| {
 805        Disclosure::new((name, row.0 as u64), !is_folded)
 806            .toggle_state(is_folded)
 807            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
 808            .into_any_element()
 809    }
 810}
 811
 812fn full_mention_for_directory(
 813    project: &Entity<Project>,
 814    abs_path: &Path,
 815    cx: &mut App,
 816) -> Task<Result<Mention>> {
 817    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
 818        let mut files = Vec::new();
 819
 820        for entry in worktree.child_entries(path) {
 821            if entry.is_dir() {
 822                files.extend(collect_files_in_path(worktree, &entry.path));
 823            } else if entry.is_file() {
 824                files.push((
 825                    entry.path.clone(),
 826                    worktree
 827                        .full_path(&entry.path)
 828                        .to_string_lossy()
 829                        .to_string(),
 830                ));
 831            }
 832        }
 833
 834        files
 835    }
 836
 837    let Some(project_path) = project
 838        .read(cx)
 839        .project_path_for_absolute_path(&abs_path, cx)
 840    else {
 841        return Task::ready(Err(anyhow!("project path not found")));
 842    };
 843    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
 844        return Task::ready(Err(anyhow!("project entry not found")));
 845    };
 846    let directory_path = entry.path.clone();
 847    let worktree_id = project_path.worktree_id;
 848    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
 849        return Task::ready(Err(anyhow!("worktree not found")));
 850    };
 851    let project = project.clone();
 852    cx.spawn(async move |cx| {
 853        let file_paths = worktree.read_with(cx, |worktree, _cx| {
 854            collect_files_in_path(worktree, &directory_path)
 855        })?;
 856        let descendants_future = cx.update(|cx| {
 857            futures::future::join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
 858                let rel_path = worktree_path
 859                    .strip_prefix(&directory_path)
 860                    .log_err()
 861                    .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
 862
 863                let open_task = project.update(cx, |project, cx| {
 864                    project.buffer_store().update(cx, |buffer_store, cx| {
 865                        let project_path = ProjectPath {
 866                            worktree_id,
 867                            path: worktree_path,
 868                        };
 869                        buffer_store.open_buffer(project_path, cx)
 870                    })
 871                });
 872
 873                cx.spawn(async move |cx| {
 874                    let buffer = open_task.await.log_err()?;
 875                    let buffer_content = outline::get_buffer_content_or_outline(
 876                        buffer.clone(),
 877                        Some(&full_path),
 878                        &cx,
 879                    )
 880                    .await
 881                    .ok()?;
 882
 883                    Some((rel_path, full_path, buffer_content.text, buffer))
 884                })
 885            }))
 886        })?;
 887
 888        let contents = cx
 889            .background_spawn(async move {
 890                let (contents, tracked_buffers) = descendants_future
 891                    .await
 892                    .into_iter()
 893                    .flatten()
 894                    .map(|(rel_path, full_path, rope, buffer)| {
 895                        ((rel_path, full_path, rope), buffer)
 896                    })
 897                    .unzip();
 898                Mention::Text {
 899                    content: render_directory_contents(contents),
 900                    tracked_buffers,
 901                }
 902            })
 903            .await;
 904        anyhow::Ok(contents)
 905    })
 906}
 907
 908fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
 909    let mut output = String::new();
 910    for (_relative_path, full_path, content) in entries {
 911        let fence = codeblock_fence_for_path(Some(&full_path), None);
 912        write!(output, "\n{fence}\n{content}\n```").unwrap();
 913    }
 914    output
 915}
 916
 917fn render_mention_fold_button(
 918    label: SharedString,
 919    icon: SharedString,
 920    range: Range<Anchor>,
 921    mut loading_finished: postage::barrier::Receiver,
 922    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
 923    editor: WeakEntity<Editor>,
 924    cx: &mut App,
 925) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
 926    let loading = cx.new(|cx| {
 927        let loading = cx.spawn(async move |this, cx| {
 928            loading_finished.recv().await;
 929            this.update(cx, |this: &mut LoadingContext, cx| {
 930                this.loading = None;
 931                cx.notify();
 932            })
 933            .ok();
 934        });
 935        LoadingContext {
 936            id: cx.entity_id(),
 937            label,
 938            icon,
 939            range,
 940            editor,
 941            loading: Some(loading),
 942            image: image_task.clone(),
 943        }
 944    });
 945    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
 946}
 947
 948struct LoadingContext {
 949    id: EntityId,
 950    label: SharedString,
 951    icon: SharedString,
 952    range: Range<Anchor>,
 953    editor: WeakEntity<Editor>,
 954    loading: Option<Task<()>>,
 955    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
 956}
 957
 958impl Render for LoadingContext {
 959    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 960        let is_in_text_selection = self
 961            .editor
 962            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
 963            .unwrap_or_default();
 964        ButtonLike::new(("loading-context", self.id))
 965            .style(ButtonStyle::Filled)
 966            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 967            .toggle_state(is_in_text_selection)
 968            .when_some(self.image.clone(), |el, image_task| {
 969                el.hoverable_tooltip(move |_, cx| {
 970                    let image = image_task.peek().cloned().transpose().ok().flatten();
 971                    let image_task = image_task.clone();
 972                    cx.new::<ImageHover>(|cx| ImageHover {
 973                        image,
 974                        _task: cx.spawn(async move |this, cx| {
 975                            if let Ok(image) = image_task.clone().await {
 976                                this.update(cx, |this, cx| {
 977                                    if this.image.replace(image).is_none() {
 978                                        cx.notify();
 979                                    }
 980                                })
 981                                .ok();
 982                            }
 983                        }),
 984                    })
 985                    .into()
 986                })
 987            })
 988            .child(
 989                h_flex()
 990                    .gap_1()
 991                    .child(
 992                        Icon::from_path(self.icon.clone())
 993                            .size(IconSize::XSmall)
 994                            .color(Color::Muted),
 995                    )
 996                    .child(
 997                        Label::new(self.label.clone())
 998                            .size(LabelSize::Small)
 999                            .buffer_font(cx)
1000                            .single_line(),
1001                    )
1002                    .map(|el| {
1003                        if self.loading.is_some() {
1004                            el.with_animation(
1005                                "loading-context-crease",
1006                                Animation::new(Duration::from_secs(2))
1007                                    .repeat()
1008                                    .with_easing(pulsating_between(0.4, 0.8)),
1009                                |label, delta| label.opacity(delta),
1010                            )
1011                            .into_any()
1012                        } else {
1013                            el.into_any()
1014                        }
1015                    }),
1016            )
1017    }
1018}
1019
1020struct ImageHover {
1021    image: Option<Arc<Image>>,
1022    _task: Task<()>,
1023}
1024
1025impl Render for ImageHover {
1026    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1027        if let Some(image) = self.image.clone() {
1028            gpui::img(image).max_w_96().max_h_96().into_any_element()
1029        } else {
1030            gpui::Empty.into_any_element()
1031        }
1032    }
1033}
1034
1035async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
1036    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
1037    enum ContentType {
1038        Html,
1039        Plaintext,
1040        Json,
1041    }
1042    use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
1043
1044    let url = if !url.starts_with("https://") && !url.starts_with("http://") {
1045        format!("https://{url}")
1046    } else {
1047        url
1048    };
1049
1050    let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
1051    let mut body = Vec::new();
1052    response
1053        .body_mut()
1054        .read_to_end(&mut body)
1055        .await
1056        .context("error reading response body")?;
1057
1058    if response.status().is_client_error() {
1059        let text = String::from_utf8_lossy(body.as_slice());
1060        anyhow::bail!(
1061            "status error {}, response: {text:?}",
1062            response.status().as_u16()
1063        );
1064    }
1065
1066    let Some(content_type) = response.headers().get("content-type") else {
1067        anyhow::bail!("missing Content-Type header");
1068    };
1069    let content_type = content_type
1070        .to_str()
1071        .context("invalid Content-Type header")?;
1072    let content_type = match content_type {
1073        "text/html" => ContentType::Html,
1074        "text/plain" => ContentType::Plaintext,
1075        "application/json" => ContentType::Json,
1076        _ => ContentType::Html,
1077    };
1078
1079    match content_type {
1080        ContentType::Html => {
1081            let mut handlers: Vec<TagHandler> = vec![
1082                Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
1083                Rc::new(RefCell::new(markdown::ParagraphHandler)),
1084                Rc::new(RefCell::new(markdown::HeadingHandler)),
1085                Rc::new(RefCell::new(markdown::ListHandler)),
1086                Rc::new(RefCell::new(markdown::TableHandler::new())),
1087                Rc::new(RefCell::new(markdown::StyledTextHandler)),
1088            ];
1089            if url.contains("wikipedia.org") {
1090                use html_to_markdown::structure::wikipedia;
1091
1092                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
1093                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
1094                handlers.push(Rc::new(
1095                    RefCell::new(wikipedia::WikipediaCodeHandler::new()),
1096                ));
1097            } else {
1098                handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
1099            }
1100            convert_html_to_markdown(&body[..], &mut handlers)
1101        }
1102        ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
1103        ContentType::Json => {
1104            let json: serde_json::Value = serde_json::from_slice(&body)?;
1105
1106            Ok(format!(
1107                "```json\n{}\n```",
1108                serde_json::to_string_pretty(&json)?
1109            ))
1110        }
1111    }
1112}