message_editor.rs

   1use crate::{
   2    acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
   3    context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
   4};
   5use acp_thread::{MentionUri, selection_name};
   6use agent_client_protocol as acp;
   7use agent_servers::{AgentServer, AgentServerDelegate};
   8use agent2::HistoryStore;
   9use anyhow::{Result, anyhow};
  10use assistant_slash_commands::codeblock_fence_for_path;
  11use assistant_tool::outline;
  12use collections::{HashMap, HashSet};
  13use editor::{
  14    Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
  15    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
  16    MultiBuffer, ToOffset,
  17    actions::Paste,
  18    display_map::{Crease, CreaseId, FoldId, Inlay},
  19};
  20use futures::{
  21    FutureExt as _,
  22    future::{Shared, join_all},
  23};
  24use gpui::{
  25    Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
  26    EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
  27    Subscription, Task, TextStyle, WeakEntity, pulsating_between,
  28};
  29use language::{Buffer, Language, language_settings::InlayHintKind};
  30use language_model::LanguageModelImage;
  31use postage::stream::Stream as _;
  32use project::{
  33    CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
  34};
  35use prompt_store::{PromptId, PromptStore};
  36use rope::Point;
  37use settings::Settings;
  38use std::{
  39    cell::RefCell,
  40    ffi::OsStr,
  41    fmt::Write,
  42    ops::{Range, RangeInclusive},
  43    path::{Path, PathBuf},
  44    rc::Rc,
  45    sync::Arc,
  46    time::Duration,
  47};
  48use text::OffsetRangeExt;
  49use theme::ThemeSettings;
  50use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
  51use util::{ResultExt, debug_panic, paths::PathStyle, rel_path::RelPath};
  52use workspace::{Workspace, notifications::NotifyResultExt as _};
  53use zed_actions::agent::Chat;
  54
  55pub struct MessageEditor {
  56    mention_set: MentionSet,
  57    editor: Entity<Editor>,
  58    project: Entity<Project>,
  59    workspace: WeakEntity<Workspace>,
  60    history_store: Entity<HistoryStore>,
  61    prompt_store: Option<Entity<PromptStore>>,
  62    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  63    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  64    agent_name: SharedString,
  65    _subscriptions: Vec<Subscription>,
  66    _parse_slash_command_task: Task<()>,
  67}
  68
  69#[derive(Clone, Copy, Debug)]
  70pub enum MessageEditorEvent {
  71    Send,
  72    Cancel,
  73    Focus,
  74    LostFocus,
  75}
  76
  77impl EventEmitter<MessageEditorEvent> for MessageEditor {}
  78
  79const COMMAND_HINT_INLAY_ID: usize = 0;
  80
  81impl MessageEditor {
  82    pub fn new(
  83        workspace: WeakEntity<Workspace>,
  84        project: Entity<Project>,
  85        history_store: Entity<HistoryStore>,
  86        prompt_store: Option<Entity<PromptStore>>,
  87        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  88        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  89        agent_name: SharedString,
  90        placeholder: &str,
  91        mode: EditorMode,
  92        window: &mut Window,
  93        cx: &mut Context<Self>,
  94    ) -> Self {
  95        let language = Language::new(
  96            language::LanguageConfig {
  97                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
  98                ..Default::default()
  99            },
 100            None,
 101        );
 102        let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
 103            cx.weak_entity(),
 104            workspace.clone(),
 105            history_store.clone(),
 106            prompt_store.clone(),
 107            prompt_capabilities.clone(),
 108            available_commands.clone(),
 109        ));
 110        let mention_set = MentionSet::default();
 111        // TODO: fix mentions when remoting with mixed path styles.
 112        let host_and_guest_paths_differ = project
 113            .read(cx)
 114            .remote_client()
 115            .is_some_and(|client| client.read(cx).path_style() != PathStyle::local());
 116        let editor = cx.new(|cx| {
 117            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 118            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 119
 120            let mut editor = Editor::new(mode, buffer, None, window, cx);
 121            editor.set_placeholder_text(placeholder, window, cx);
 122            editor.set_show_indent_guides(false, cx);
 123            editor.set_soft_wrap();
 124            editor.set_use_modal_editing(true);
 125            if !host_and_guest_paths_differ {
 126                editor.set_completion_provider(Some(completion_provider.clone()));
 127            }
 128            editor.set_context_menu_options(ContextMenuOptions {
 129                min_entries_visible: 12,
 130                max_entries_visible: 12,
 131                placement: Some(ContextMenuPlacement::Above),
 132            });
 133            editor.register_addon(MessageEditorAddon::new());
 134            editor
 135        });
 136
 137        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 138            cx.emit(MessageEditorEvent::Focus)
 139        })
 140        .detach();
 141        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 142            cx.emit(MessageEditorEvent::LostFocus)
 143        })
 144        .detach();
 145
 146        let mut has_hint = false;
 147        let mut subscriptions = Vec::new();
 148
 149        subscriptions.push(cx.subscribe_in(&editor, window, {
 150            move |this, editor, event, window, cx| {
 151                if let EditorEvent::Edited { .. } = event {
 152                    let snapshot = editor.update(cx, |editor, cx| {
 153                        let new_hints = this
 154                            .command_hint(editor.buffer(), cx)
 155                            .into_iter()
 156                            .collect::<Vec<_>>();
 157                        let has_new_hint = !new_hints.is_empty();
 158                        editor.splice_inlays(
 159                            if has_hint {
 160                                &[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
 161                            } else {
 162                                &[]
 163                            },
 164                            new_hints,
 165                            cx,
 166                        );
 167                        has_hint = has_new_hint;
 168
 169                        editor.snapshot(window, cx)
 170                    });
 171                    this.mention_set.remove_invalid(snapshot);
 172
 173                    cx.notify();
 174                }
 175            }
 176        }));
 177
 178        Self {
 179            editor,
 180            project,
 181            mention_set,
 182            workspace,
 183            history_store,
 184            prompt_store,
 185            prompt_capabilities,
 186            available_commands,
 187            agent_name,
 188            _subscriptions: subscriptions,
 189            _parse_slash_command_task: Task::ready(()),
 190        }
 191    }
 192
 193    fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
 194        let available_commands = self.available_commands.borrow();
 195        if available_commands.is_empty() {
 196            return None;
 197        }
 198
 199        let snapshot = buffer.read(cx).snapshot(cx);
 200        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 201        if parsed_command.argument.is_some() {
 202            return None;
 203        }
 204
 205        let command_name = parsed_command.command?;
 206        let available_command = available_commands
 207            .iter()
 208            .find(|command| command.name == command_name)?;
 209
 210        let acp::AvailableCommandInput::Unstructured { mut hint } =
 211            available_command.input.clone()?;
 212
 213        let mut hint_pos = parsed_command.source_range.end + 1;
 214        if hint_pos > snapshot.len() {
 215            hint_pos = snapshot.len();
 216            hint.insert(0, ' ');
 217        }
 218
 219        let hint_pos = snapshot.anchor_after(hint_pos);
 220
 221        Some(Inlay::hint(
 222            COMMAND_HINT_INLAY_ID,
 223            hint_pos,
 224            &InlayHint {
 225                position: hint_pos.text_anchor,
 226                label: InlayHintLabel::String(hint),
 227                kind: Some(InlayHintKind::Parameter),
 228                padding_left: false,
 229                padding_right: false,
 230                tooltip: None,
 231                resolve_state: project::ResolveState::Resolved,
 232            },
 233        ))
 234    }
 235
 236    pub fn insert_thread_summary(
 237        &mut self,
 238        thread: agent2::DbThreadMetadata,
 239        window: &mut Window,
 240        cx: &mut Context<Self>,
 241    ) {
 242        let start = self.editor.update(cx, |editor, cx| {
 243            editor.set_text(format!("{}\n", thread.title), window, cx);
 244            editor
 245                .buffer()
 246                .read(cx)
 247                .snapshot(cx)
 248                .anchor_before(Point::zero())
 249                .text_anchor
 250        });
 251
 252        self.confirm_mention_completion(
 253            thread.title.clone(),
 254            start,
 255            thread.title.len(),
 256            MentionUri::Thread {
 257                id: thread.id.clone(),
 258                name: thread.title.to_string(),
 259            },
 260            window,
 261            cx,
 262        )
 263        .detach();
 264    }
 265
 266    #[cfg(test)]
 267    pub(crate) fn editor(&self) -> &Entity<Editor> {
 268        &self.editor
 269    }
 270
 271    #[cfg(test)]
 272    pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
 273        &mut self.mention_set
 274    }
 275
 276    pub fn is_empty(&self, cx: &App) -> bool {
 277        self.editor.read(cx).is_empty(cx)
 278    }
 279
 280    pub fn mentions(&self) -> HashSet<MentionUri> {
 281        self.mention_set
 282            .mentions
 283            .values()
 284            .map(|(uri, _)| uri.clone())
 285            .collect()
 286    }
 287
 288    pub fn confirm_mention_completion(
 289        &mut self,
 290        crease_text: SharedString,
 291        start: text::Anchor,
 292        content_len: usize,
 293        mention_uri: MentionUri,
 294        window: &mut Window,
 295        cx: &mut Context<Self>,
 296    ) -> Task<()> {
 297        let snapshot = self
 298            .editor
 299            .update(cx, |editor, cx| editor.snapshot(window, cx));
 300        let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
 301            return Task::ready(());
 302        };
 303        let Some(start_anchor) = snapshot
 304            .buffer_snapshot
 305            .anchor_in_excerpt(*excerpt_id, start)
 306        else {
 307            return Task::ready(());
 308        };
 309        let end_anchor = snapshot
 310            .buffer_snapshot
 311            .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
 312
 313        let crease = if let MentionUri::File { abs_path } = &mention_uri
 314            && let Some(extension) = abs_path.extension()
 315            && let Some(extension) = extension.to_str()
 316            && Img::extensions().contains(&extension)
 317            && !extension.contains("svg")
 318        {
 319            let Some(project_path) = self
 320                .project
 321                .read(cx)
 322                .project_path_for_absolute_path(&abs_path, cx)
 323            else {
 324                log::error!("project path not found");
 325                return Task::ready(());
 326            };
 327            let image = self
 328                .project
 329                .update(cx, |project, cx| project.open_image(project_path, cx));
 330            let image = cx
 331                .spawn(async move |_, cx| {
 332                    let image = image.await.map_err(|e| e.to_string())?;
 333                    let image = image
 334                        .update(cx, |image, _| image.image.clone())
 335                        .map_err(|e| e.to_string())?;
 336                    Ok(image)
 337                })
 338                .shared();
 339            insert_crease_for_mention(
 340                *excerpt_id,
 341                start,
 342                content_len,
 343                mention_uri.name().into(),
 344                IconName::Image.path().into(),
 345                Some(image),
 346                self.editor.clone(),
 347                window,
 348                cx,
 349            )
 350        } else {
 351            insert_crease_for_mention(
 352                *excerpt_id,
 353                start,
 354                content_len,
 355                crease_text,
 356                mention_uri.icon_path(cx),
 357                None,
 358                self.editor.clone(),
 359                window,
 360                cx,
 361            )
 362        };
 363        let Some((crease_id, tx)) = crease else {
 364            return Task::ready(());
 365        };
 366
 367        let task = match mention_uri.clone() {
 368            MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
 369            MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
 370            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
 371            MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
 372            MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
 373            MentionUri::Symbol {
 374                abs_path,
 375                line_range,
 376                ..
 377            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
 378            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
 379            MentionUri::PastedImage => {
 380                debug_panic!("pasted image URI should not be included in completions");
 381                Task::ready(Err(anyhow!(
 382                    "pasted imaged URI should not be included in completions"
 383                )))
 384            }
 385            MentionUri::Selection { .. } => {
 386                // Handled elsewhere
 387                debug_panic!("unexpected selection URI");
 388                Task::ready(Err(anyhow!("unexpected selection URI")))
 389            }
 390        };
 391        let task = cx
 392            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
 393            .shared();
 394        self.mention_set
 395            .mentions
 396            .insert(crease_id, (mention_uri, task.clone()));
 397
 398        // Notify the user if we failed to load the mentioned context
 399        cx.spawn_in(window, async move |this, cx| {
 400            let result = task.await.notify_async_err(cx);
 401            drop(tx);
 402            if result.is_none() {
 403                this.update(cx, |this, cx| {
 404                    this.editor.update(cx, |editor, cx| {
 405                        // Remove mention
 406                        editor.edit([(start_anchor..end_anchor, "")], cx);
 407                    });
 408                    this.mention_set.mentions.remove(&crease_id);
 409                })
 410                .ok();
 411            }
 412        })
 413    }
 414
 415    fn confirm_mention_for_file(
 416        &mut self,
 417        abs_path: PathBuf,
 418        cx: &mut Context<Self>,
 419    ) -> Task<Result<Mention>> {
 420        let Some(project_path) = self
 421            .project
 422            .read(cx)
 423            .project_path_for_absolute_path(&abs_path, cx)
 424        else {
 425            return Task::ready(Err(anyhow!("project path not found")));
 426        };
 427        let extension = abs_path
 428            .extension()
 429            .and_then(OsStr::to_str)
 430            .unwrap_or_default();
 431
 432        if Img::extensions().contains(&extension) && !extension.contains("svg") {
 433            if !self.prompt_capabilities.borrow().image {
 434                return Task::ready(Err(anyhow!("This model does not support images yet")));
 435            }
 436            let task = self
 437                .project
 438                .update(cx, |project, cx| project.open_image(project_path, cx));
 439            return cx.spawn(async move |_, cx| {
 440                let image = task.await?;
 441                let image = image.update(cx, |image, _| image.image.clone())?;
 442                let format = image.format;
 443                let image = cx
 444                    .update(|cx| LanguageModelImage::from_image(image, cx))?
 445                    .await;
 446                if let Some(image) = image {
 447                    Ok(Mention::Image(MentionImage {
 448                        data: image.source,
 449                        format,
 450                    }))
 451                } else {
 452                    Err(anyhow!("Failed to convert image"))
 453                }
 454            });
 455        }
 456
 457        let buffer = self
 458            .project
 459            .update(cx, |project, cx| project.open_buffer(project_path, cx));
 460        cx.spawn(async move |_, cx| {
 461            let buffer = buffer.await?;
 462            let buffer_content =
 463                outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
 464                    .await?;
 465
 466            Ok(Mention::Text {
 467                content: buffer_content.text,
 468                tracked_buffers: vec![buffer],
 469            })
 470        })
 471    }
 472
 473    fn confirm_mention_for_fetch(
 474        &mut self,
 475        url: url::Url,
 476        cx: &mut Context<Self>,
 477    ) -> Task<Result<Mention>> {
 478        let http_client = match self
 479            .workspace
 480            .update(cx, |workspace, _| workspace.client().http_client())
 481        {
 482            Ok(http_client) => http_client,
 483            Err(e) => return Task::ready(Err(e)),
 484        };
 485        cx.background_executor().spawn(async move {
 486            let content = fetch_url_content(http_client, url.to_string()).await?;
 487            Ok(Mention::Text {
 488                content,
 489                tracked_buffers: Vec::new(),
 490            })
 491        })
 492    }
 493
 494    fn confirm_mention_for_symbol(
 495        &mut self,
 496        abs_path: PathBuf,
 497        line_range: RangeInclusive<u32>,
 498        cx: &mut Context<Self>,
 499    ) -> Task<Result<Mention>> {
 500        let Some(project_path) = self
 501            .project
 502            .read(cx)
 503            .project_path_for_absolute_path(&abs_path, cx)
 504        else {
 505            return Task::ready(Err(anyhow!("project path not found")));
 506        };
 507        let buffer = self
 508            .project
 509            .update(cx, |project, cx| project.open_buffer(project_path, cx));
 510        cx.spawn(async move |_, cx| {
 511            let buffer = buffer.await?;
 512            let mention = buffer.update(cx, |buffer, cx| {
 513                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
 514                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
 515                let content = buffer.text_for_range(start..end).collect();
 516                Mention::Text {
 517                    content,
 518                    tracked_buffers: vec![cx.entity()],
 519                }
 520            })?;
 521            anyhow::Ok(mention)
 522        })
 523    }
 524
 525    fn confirm_mention_for_rule(
 526        &mut self,
 527        id: PromptId,
 528        cx: &mut Context<Self>,
 529    ) -> Task<Result<Mention>> {
 530        let Some(prompt_store) = self.prompt_store.clone() else {
 531            return Task::ready(Err(anyhow!("missing prompt store")));
 532        };
 533        let prompt = prompt_store.read(cx).load(id, cx);
 534        cx.spawn(async move |_, _| {
 535            let prompt = prompt.await?;
 536            Ok(Mention::Text {
 537                content: prompt,
 538                tracked_buffers: Vec::new(),
 539            })
 540        })
 541    }
 542
 543    pub fn confirm_mention_for_selection(
 544        &mut self,
 545        source_range: Range<text::Anchor>,
 546        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
 547        window: &mut Window,
 548        cx: &mut Context<Self>,
 549    ) {
 550        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
 551        let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
 552            return;
 553        };
 554        let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
 555            return;
 556        };
 557
 558        let offset = start.to_offset(&snapshot);
 559
 560        for (buffer, selection_range, range_to_fold) in selections {
 561            let range = snapshot.anchor_after(offset + range_to_fold.start)
 562                ..snapshot.anchor_after(offset + range_to_fold.end);
 563
 564            let abs_path = buffer
 565                .read(cx)
 566                .project_path(cx)
 567                .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx));
 568            let snapshot = buffer.read(cx).snapshot();
 569
 570            let text = snapshot
 571                .text_for_range(selection_range.clone())
 572                .collect::<String>();
 573            let point_range = selection_range.to_point(&snapshot);
 574            let line_range = point_range.start.row..=point_range.end.row;
 575
 576            let uri = MentionUri::Selection {
 577                abs_path: abs_path.clone(),
 578                line_range: line_range.clone(),
 579            };
 580            let crease = crate::context_picker::crease_for_mention(
 581                selection_name(abs_path.as_deref(), &line_range).into(),
 582                uri.icon_path(cx),
 583                range,
 584                self.editor.downgrade(),
 585            );
 586
 587            let crease_id = self.editor.update(cx, |editor, cx| {
 588                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
 589                editor.fold_creases(vec![crease], false, window, cx);
 590                crease_ids.first().copied().unwrap()
 591            });
 592
 593            self.mention_set.mentions.insert(
 594                crease_id,
 595                (
 596                    uri,
 597                    Task::ready(Ok(Mention::Text {
 598                        content: text,
 599                        tracked_buffers: vec![buffer],
 600                    }))
 601                    .shared(),
 602                ),
 603            );
 604        }
 605    }
 606
 607    fn confirm_mention_for_thread(
 608        &mut self,
 609        id: acp::SessionId,
 610        cx: &mut Context<Self>,
 611    ) -> Task<Result<Mention>> {
 612        let server = Rc::new(agent2::NativeAgentServer::new(
 613            self.project.read(cx).fs().clone(),
 614            self.history_store.clone(),
 615        ));
 616        let delegate = AgentServerDelegate::new(
 617            self.project.read(cx).agent_server_store().clone(),
 618            self.project.clone(),
 619            None,
 620            None,
 621        );
 622        let connection = server.connect(None, delegate, cx);
 623        cx.spawn(async move |_, cx| {
 624            let (agent, _) = connection.await?;
 625            let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
 626            let summary = agent
 627                .0
 628                .update(cx, |agent, cx| agent.thread_summary(id, cx))?
 629                .await?;
 630            anyhow::Ok(Mention::Text {
 631                content: summary.to_string(),
 632                tracked_buffers: Vec::new(),
 633            })
 634        })
 635    }
 636
 637    fn confirm_mention_for_text_thread(
 638        &mut self,
 639        path: PathBuf,
 640        cx: &mut Context<Self>,
 641    ) -> Task<Result<Mention>> {
 642        let context = self.history_store.update(cx, |text_thread_store, cx| {
 643            text_thread_store.load_text_thread(path.as_path().into(), cx)
 644        });
 645        cx.spawn(async move |_, cx| {
 646            let context = context.await?;
 647            let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
 648            Ok(Mention::Text {
 649                content: xml,
 650                tracked_buffers: Vec::new(),
 651            })
 652        })
 653    }
 654
 655    fn validate_slash_commands(
 656        text: &str,
 657        available_commands: &[acp::AvailableCommand],
 658        agent_name: &str,
 659    ) -> Result<()> {
 660        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 661            if let Some(command_name) = parsed_command.command {
 662                // Check if this command is in the list of available commands from the server
 663                let is_supported = available_commands
 664                    .iter()
 665                    .any(|cmd| cmd.name == command_name);
 666
 667                if !is_supported {
 668                    return Err(anyhow!(
 669                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 670                        command_name,
 671                        agent_name,
 672                        if available_commands.is_empty() {
 673                            "none".to_string()
 674                        } else {
 675                            available_commands
 676                                .iter()
 677                                .map(|cmd| format!("/{}", cmd.name))
 678                                .collect::<Vec<_>>()
 679                                .join(", ")
 680                        }
 681                    ));
 682                }
 683            }
 684        }
 685        Ok(())
 686    }
 687
 688    pub fn contents(
 689        &self,
 690        full_mention_content: bool,
 691        cx: &mut Context<Self>,
 692    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 693        // Check for unsupported slash commands before spawning async task
 694        let text = self.editor.read(cx).text(cx);
 695        let available_commands = self.available_commands.borrow().clone();
 696        if let Err(err) =
 697            Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
 698        {
 699            return Task::ready(Err(err));
 700        }
 701
 702        let contents = self.mention_set.contents(
 703            &self.prompt_capabilities.borrow(),
 704            full_mention_content,
 705            self.project.clone(),
 706            cx,
 707        );
 708        let editor = self.editor.clone();
 709
 710        cx.spawn(async move |_, cx| {
 711            let contents = contents.await?;
 712            let mut all_tracked_buffers = Vec::new();
 713
 714            let result = editor.update(cx, |editor, cx| {
 715                let mut ix = 0;
 716                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 717                let text = editor.text(cx);
 718                editor.display_map.update(cx, |map, cx| {
 719                    let snapshot = map.snapshot(cx);
 720                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 721                        let Some((uri, mention)) = contents.get(&crease_id) else {
 722                            continue;
 723                        };
 724
 725                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
 726                        if crease_range.start > ix {
 727                            //todo(): Custom slash command ContentBlock?
 728                            // let chunk = if prevent_slash_commands
 729                            //     && ix == 0
 730                            //     && parse_slash_command(&text[ix..]).is_some()
 731                            // {
 732                            //     format!(" {}", &text[ix..crease_range.start]).into()
 733                            // } else {
 734                            //     text[ix..crease_range.start].into()
 735                            // };
 736                            let chunk = text[ix..crease_range.start].into();
 737                            chunks.push(chunk);
 738                        }
 739                        let chunk = match mention {
 740                            Mention::Text {
 741                                content,
 742                                tracked_buffers,
 743                            } => {
 744                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 745                                acp::ContentBlock::Resource(acp::EmbeddedResource {
 746                                    annotations: None,
 747                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
 748                                        acp::TextResourceContents {
 749                                            mime_type: None,
 750                                            text: content.clone(),
 751                                            uri: uri.to_uri().to_string(),
 752                                            meta: None,
 753                                        },
 754                                    ),
 755                                    meta: None,
 756                                })
 757                            }
 758                            Mention::Image(mention_image) => {
 759                                let uri = match uri {
 760                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 761                                    MentionUri::PastedImage => None,
 762                                    other => {
 763                                        debug_panic!(
 764                                            "unexpected mention uri for image: {:?}",
 765                                            other
 766                                        );
 767                                        None
 768                                    }
 769                                };
 770                                acp::ContentBlock::Image(acp::ImageContent {
 771                                    annotations: None,
 772                                    data: mention_image.data.to_string(),
 773                                    mime_type: mention_image.format.mime_type().into(),
 774                                    uri,
 775                                    meta: None,
 776                                })
 777                            }
 778                            Mention::UriOnly => {
 779                                acp::ContentBlock::ResourceLink(acp::ResourceLink {
 780                                    name: uri.name(),
 781                                    uri: uri.to_uri().to_string(),
 782                                    annotations: None,
 783                                    description: None,
 784                                    mime_type: None,
 785                                    size: None,
 786                                    title: None,
 787                                    meta: None,
 788                                })
 789                            }
 790                        };
 791                        chunks.push(chunk);
 792                        ix = crease_range.end;
 793                    }
 794
 795                    if ix < text.len() {
 796                        //todo(): Custom slash command ContentBlock?
 797                        // let last_chunk = if prevent_slash_commands
 798                        //     && ix == 0
 799                        //     && parse_slash_command(&text[ix..]).is_some()
 800                        // {
 801                        //     format!(" {}", text[ix..].trim_end())
 802                        // } else {
 803                        //     text[ix..].trim_end().to_owned()
 804                        // };
 805                        let last_chunk = text[ix..].trim_end().to_owned();
 806                        if !last_chunk.is_empty() {
 807                            chunks.push(last_chunk.into());
 808                        }
 809                    }
 810                });
 811                Ok((chunks, all_tracked_buffers))
 812            })?;
 813            result
 814        })
 815    }
 816
 817    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 818        self.editor.update(cx, |editor, cx| {
 819            editor.clear(window, cx);
 820            editor.remove_creases(
 821                self.mention_set
 822                    .mentions
 823                    .drain()
 824                    .map(|(crease_id, _)| crease_id),
 825                cx,
 826            )
 827        });
 828    }
 829
 830    fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 831        if self.is_empty(cx) {
 832            return;
 833        }
 834        cx.emit(MessageEditorEvent::Send)
 835    }
 836
 837    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 838        cx.emit(MessageEditorEvent::Cancel)
 839    }
 840
 841    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 842        if !self.prompt_capabilities.borrow().image {
 843            return;
 844        }
 845
 846        let images = cx
 847            .read_from_clipboard()
 848            .map(|item| {
 849                item.into_entries()
 850                    .filter_map(|entry| {
 851                        if let ClipboardEntry::Image(image) = entry {
 852                            Some(image)
 853                        } else {
 854                            None
 855                        }
 856                    })
 857                    .collect::<Vec<_>>()
 858            })
 859            .unwrap_or_default();
 860
 861        if images.is_empty() {
 862            return;
 863        }
 864        cx.stop_propagation();
 865
 866        let replacement_text = MentionUri::PastedImage.as_link().to_string();
 867        for image in images {
 868            let (excerpt_id, text_anchor, multibuffer_anchor) =
 869                self.editor.update(cx, |message_editor, cx| {
 870                    let snapshot = message_editor.snapshot(window, cx);
 871                    let (excerpt_id, _, buffer_snapshot) =
 872                        snapshot.buffer_snapshot.as_singleton().unwrap();
 873
 874                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 875                    let multibuffer_anchor = snapshot
 876                        .buffer_snapshot
 877                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 878                    message_editor.edit(
 879                        [(
 880                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 881                            format!("{replacement_text} "),
 882                        )],
 883                        cx,
 884                    );
 885                    (*excerpt_id, text_anchor, multibuffer_anchor)
 886                });
 887
 888            let content_len = replacement_text.len();
 889            let Some(start_anchor) = multibuffer_anchor else {
 890                continue;
 891            };
 892            let end_anchor = self.editor.update(cx, |editor, cx| {
 893                let snapshot = editor.buffer().read(cx).snapshot(cx);
 894                snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
 895            });
 896            let image = Arc::new(image);
 897            let Some((crease_id, tx)) = insert_crease_for_mention(
 898                excerpt_id,
 899                text_anchor,
 900                content_len,
 901                MentionUri::PastedImage.name().into(),
 902                IconName::Image.path().into(),
 903                Some(Task::ready(Ok(image.clone())).shared()),
 904                self.editor.clone(),
 905                window,
 906                cx,
 907            ) else {
 908                continue;
 909            };
 910            let task = cx
 911                .spawn_in(window, {
 912                    async move |_, cx| {
 913                        let format = image.format;
 914                        let image = cx
 915                            .update(|_, cx| LanguageModelImage::from_image(image, cx))
 916                            .map_err(|e| e.to_string())?
 917                            .await;
 918                        drop(tx);
 919                        if let Some(image) = image {
 920                            Ok(Mention::Image(MentionImage {
 921                                data: image.source,
 922                                format,
 923                            }))
 924                        } else {
 925                            Err("Failed to convert image".into())
 926                        }
 927                    }
 928                })
 929                .shared();
 930
 931            self.mention_set
 932                .mentions
 933                .insert(crease_id, (MentionUri::PastedImage, task.clone()));
 934
 935            cx.spawn_in(window, async move |this, cx| {
 936                if task.await.notify_async_err(cx).is_none() {
 937                    this.update(cx, |this, cx| {
 938                        this.editor.update(cx, |editor, cx| {
 939                            editor.edit([(start_anchor..end_anchor, "")], cx);
 940                        });
 941                        this.mention_set.mentions.remove(&crease_id);
 942                    })
 943                    .ok();
 944                }
 945            })
 946            .detach();
 947        }
 948    }
 949
 950    pub fn insert_dragged_files(
 951        &mut self,
 952        paths: Vec<project::ProjectPath>,
 953        added_worktrees: Vec<Entity<Worktree>>,
 954        window: &mut Window,
 955        cx: &mut Context<Self>,
 956    ) {
 957        let path_style = self.project.read(cx).path_style(cx);
 958        let buffer = self.editor.read(cx).buffer().clone();
 959        let Some(buffer) = buffer.read(cx).as_singleton() else {
 960            return;
 961        };
 962        let mut tasks = Vec::new();
 963        for path in paths {
 964            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
 965                continue;
 966            };
 967            let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 968                continue;
 969            };
 970            let abs_path = worktree.read(cx).absolutize(&path.path);
 971            let (file_name, _) =
 972                crate::context_picker::file_context_picker::extract_file_name_and_directory(
 973                    &path.path,
 974                    worktree.read(cx).root_name(),
 975                    path_style,
 976                );
 977
 978            let uri = if entry.is_dir() {
 979                MentionUri::Directory { abs_path }
 980            } else {
 981                MentionUri::File { abs_path }
 982            };
 983
 984            let new_text = format!("{} ", uri.as_link());
 985            let content_len = new_text.len() - 1;
 986
 987            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 988
 989            self.editor.update(cx, |message_editor, cx| {
 990                message_editor.edit(
 991                    [(
 992                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 993                        new_text,
 994                    )],
 995                    cx,
 996                );
 997            });
 998            tasks.push(self.confirm_mention_completion(
 999                file_name,
1000                anchor,
1001                content_len,
1002                uri,
1003                window,
1004                cx,
1005            ));
1006        }
1007        cx.spawn(async move |_, _| {
1008            join_all(tasks).await;
1009            drop(added_worktrees);
1010        })
1011        .detach();
1012    }
1013
1014    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1015        let editor = self.editor.read(cx);
1016        let editor_buffer = editor.buffer().read(cx);
1017        let Some(buffer) = editor_buffer.as_singleton() else {
1018            return;
1019        };
1020        let cursor_anchor = editor.selections.newest_anchor().head();
1021        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1022        let anchor = buffer.update(cx, |buffer, _cx| {
1023            buffer.anchor_before(cursor_offset.min(buffer.len()))
1024        });
1025        let Some(workspace) = self.workspace.upgrade() else {
1026            return;
1027        };
1028        let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
1029            ContextPickerAction::AddSelections,
1030            anchor..anchor,
1031            cx.weak_entity(),
1032            &workspace,
1033            cx,
1034        ) else {
1035            return;
1036        };
1037        self.editor.update(cx, |message_editor, cx| {
1038            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1039        });
1040        if let Some(confirm) = completion.confirm {
1041            confirm(CompletionIntent::Complete, window, cx);
1042        }
1043    }
1044
1045    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1046        self.editor.update(cx, |message_editor, cx| {
1047            message_editor.set_read_only(read_only);
1048            cx.notify()
1049        })
1050    }
1051
1052    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1053        self.editor.update(cx, |editor, cx| {
1054            editor.set_mode(mode);
1055            cx.notify()
1056        });
1057    }
1058
1059    pub fn set_message(
1060        &mut self,
1061        message: Vec<acp::ContentBlock>,
1062        window: &mut Window,
1063        cx: &mut Context<Self>,
1064    ) {
1065        self.clear(window, cx);
1066
1067        let mut text = String::new();
1068        let mut mentions = Vec::new();
1069
1070        for chunk in message {
1071            match chunk {
1072                acp::ContentBlock::Text(text_content) => {
1073                    text.push_str(&text_content.text);
1074                }
1075                acp::ContentBlock::Resource(acp::EmbeddedResource {
1076                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1077                    ..
1078                }) => {
1079                    let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else {
1080                        continue;
1081                    };
1082                    let start = text.len();
1083                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1084                    let end = text.len();
1085                    mentions.push((
1086                        start..end,
1087                        mention_uri,
1088                        Mention::Text {
1089                            content: resource.text,
1090                            tracked_buffers: Vec::new(),
1091                        },
1092                    ));
1093                }
1094                acp::ContentBlock::ResourceLink(resource) => {
1095                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
1096                        let start = text.len();
1097                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1098                        let end = text.len();
1099                        mentions.push((start..end, mention_uri, Mention::UriOnly));
1100                    }
1101                }
1102                acp::ContentBlock::Image(acp::ImageContent {
1103                    uri,
1104                    data,
1105                    mime_type,
1106                    annotations: _,
1107                    meta: _,
1108                }) => {
1109                    let mention_uri = if let Some(uri) = uri {
1110                        MentionUri::parse(&uri)
1111                    } else {
1112                        Ok(MentionUri::PastedImage)
1113                    };
1114                    let Some(mention_uri) = mention_uri.log_err() else {
1115                        continue;
1116                    };
1117                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1118                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1119                        continue;
1120                    };
1121                    let start = text.len();
1122                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1123                    let end = text.len();
1124                    mentions.push((
1125                        start..end,
1126                        mention_uri,
1127                        Mention::Image(MentionImage {
1128                            data: data.into(),
1129                            format,
1130                        }),
1131                    ));
1132                }
1133                acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
1134            }
1135        }
1136
1137        let snapshot = self.editor.update(cx, |editor, cx| {
1138            editor.set_text(text, window, cx);
1139            editor.buffer().read(cx).snapshot(cx)
1140        });
1141
1142        for (range, mention_uri, mention) in mentions {
1143            let anchor = snapshot.anchor_before(range.start);
1144            let Some((crease_id, tx)) = insert_crease_for_mention(
1145                anchor.excerpt_id,
1146                anchor.text_anchor,
1147                range.end - range.start,
1148                mention_uri.name().into(),
1149                mention_uri.icon_path(cx),
1150                None,
1151                self.editor.clone(),
1152                window,
1153                cx,
1154            ) else {
1155                continue;
1156            };
1157            drop(tx);
1158
1159            self.mention_set.mentions.insert(
1160                crease_id,
1161                (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
1162            );
1163        }
1164        cx.notify();
1165    }
1166
1167    pub fn text(&self, cx: &App) -> String {
1168        self.editor.read(cx).text(cx)
1169    }
1170
1171    #[cfg(test)]
1172    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1173        self.editor.update(cx, |editor, cx| {
1174            editor.set_text(text, window, cx);
1175        });
1176    }
1177}
1178
1179fn full_mention_for_directory(
1180    project: &Entity<Project>,
1181    abs_path: &Path,
1182    cx: &mut App,
1183) -> Task<Result<Mention>> {
1184    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, PathBuf)> {
1185        let mut files = Vec::new();
1186
1187        for entry in worktree.child_entries(path) {
1188            if entry.is_dir() {
1189                files.extend(collect_files_in_path(worktree, &entry.path));
1190            } else if entry.is_file() {
1191                files.push((entry.path.clone(), worktree.full_path(&entry.path)));
1192            }
1193        }
1194
1195        files
1196    }
1197
1198    let Some(project_path) = project
1199        .read(cx)
1200        .project_path_for_absolute_path(&abs_path, cx)
1201    else {
1202        return Task::ready(Err(anyhow!("project path not found")));
1203    };
1204    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
1205        return Task::ready(Err(anyhow!("project entry not found")));
1206    };
1207    let directory_path = entry.path.clone();
1208    let worktree_id = project_path.worktree_id;
1209    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
1210        return Task::ready(Err(anyhow!("worktree not found")));
1211    };
1212    let project = project.clone();
1213    cx.spawn(async move |cx| {
1214        let file_paths = worktree.read_with(cx, |worktree, _cx| {
1215            collect_files_in_path(worktree, &directory_path)
1216        })?;
1217        let descendants_future = cx.update(|cx| {
1218            join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
1219                let rel_path = worktree_path
1220                    .strip_prefix(&directory_path)
1221                    .log_err()
1222                    .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
1223
1224                let open_task = project.update(cx, |project, cx| {
1225                    project.buffer_store().update(cx, |buffer_store, cx| {
1226                        let project_path = ProjectPath {
1227                            worktree_id,
1228                            path: worktree_path,
1229                        };
1230                        buffer_store.open_buffer(project_path, cx)
1231                    })
1232                });
1233
1234                cx.spawn(async move |cx| {
1235                    let buffer = open_task.await.log_err()?;
1236                    let buffer_content = outline::get_buffer_content_or_outline(
1237                        buffer.clone(),
1238                        Some(&full_path),
1239                        &cx,
1240                    )
1241                    .await
1242                    .ok()?;
1243
1244                    Some((rel_path, full_path, buffer_content.text, buffer))
1245                })
1246            }))
1247        })?;
1248
1249        let contents = cx
1250            .background_spawn(async move {
1251                let (contents, tracked_buffers) = descendants_future
1252                    .await
1253                    .into_iter()
1254                    .flatten()
1255                    .map(|(rel_path, full_path, rope, buffer)| {
1256                        ((rel_path, full_path, rope), buffer)
1257                    })
1258                    .unzip();
1259                Mention::Text {
1260                    content: render_directory_contents(contents),
1261                    tracked_buffers,
1262                }
1263            })
1264            .await;
1265        anyhow::Ok(contents)
1266    })
1267}
1268
1269fn render_directory_contents(entries: Vec<(Arc<RelPath>, PathBuf, String)>) -> String {
1270    let mut output = String::new();
1271    for (_relative_path, full_path, content) in entries {
1272        let fence = codeblock_fence_for_path(Some(&full_path), None);
1273        write!(output, "\n{fence}\n{content}\n```").unwrap();
1274    }
1275    output
1276}
1277
1278impl Focusable for MessageEditor {
1279    fn focus_handle(&self, cx: &App) -> FocusHandle {
1280        self.editor.focus_handle(cx)
1281    }
1282}
1283
1284impl Render for MessageEditor {
1285    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1286        div()
1287            .key_context("MessageEditor")
1288            .on_action(cx.listener(Self::send))
1289            .on_action(cx.listener(Self::cancel))
1290            .capture_action(cx.listener(Self::paste))
1291            .flex_1()
1292            .child({
1293                let settings = ThemeSettings::get_global(cx);
1294
1295                let text_style = TextStyle {
1296                    color: cx.theme().colors().text,
1297                    font_family: settings.buffer_font.family.clone(),
1298                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1299                    font_features: settings.buffer_font.features.clone(),
1300                    font_size: settings.buffer_font_size(cx).into(),
1301                    line_height: relative(settings.buffer_line_height.value()),
1302                    ..Default::default()
1303                };
1304
1305                EditorElement::new(
1306                    &self.editor,
1307                    EditorStyle {
1308                        background: cx.theme().colors().editor_background,
1309                        local_player: cx.theme().players().local(),
1310                        text: text_style,
1311                        syntax: cx.theme().syntax().clone(),
1312                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1313                        ..Default::default()
1314                    },
1315                )
1316            })
1317    }
1318}
1319
1320pub(crate) fn insert_crease_for_mention(
1321    excerpt_id: ExcerptId,
1322    anchor: text::Anchor,
1323    content_len: usize,
1324    crease_label: SharedString,
1325    crease_icon: SharedString,
1326    // abs_path: Option<Arc<Path>>,
1327    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1328    editor: Entity<Editor>,
1329    window: &mut Window,
1330    cx: &mut App,
1331) -> Option<(CreaseId, postage::barrier::Sender)> {
1332    let (tx, rx) = postage::barrier::channel();
1333
1334    let crease_id = editor.update(cx, |editor, cx| {
1335        let snapshot = editor.buffer().read(cx).snapshot(cx);
1336
1337        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
1338
1339        let start = start.bias_right(&snapshot);
1340        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
1341
1342        let placeholder = FoldPlaceholder {
1343            render: render_mention_fold_button(
1344                crease_label,
1345                crease_icon,
1346                start..end,
1347                rx,
1348                image,
1349                cx.weak_entity(),
1350                cx,
1351            ),
1352            merge_adjacent: false,
1353            ..Default::default()
1354        };
1355
1356        let crease = Crease::Inline {
1357            range: start..end,
1358            placeholder,
1359            render_toggle: None,
1360            render_trailer: None,
1361            metadata: None,
1362        };
1363
1364        let ids = editor.insert_creases(vec![crease.clone()], cx);
1365        editor.fold_creases(vec![crease], false, window, cx);
1366
1367        Some(ids[0])
1368    })?;
1369
1370    Some((crease_id, tx))
1371}
1372
1373fn render_mention_fold_button(
1374    label: SharedString,
1375    icon: SharedString,
1376    range: Range<Anchor>,
1377    mut loading_finished: postage::barrier::Receiver,
1378    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1379    editor: WeakEntity<Editor>,
1380    cx: &mut App,
1381) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
1382    let loading = cx.new(|cx| {
1383        let loading = cx.spawn(async move |this, cx| {
1384            loading_finished.recv().await;
1385            this.update(cx, |this: &mut LoadingContext, cx| {
1386                this.loading = None;
1387                cx.notify();
1388            })
1389            .ok();
1390        });
1391        LoadingContext {
1392            id: cx.entity_id(),
1393            label,
1394            icon,
1395            range,
1396            editor,
1397            loading: Some(loading),
1398            image: image_task.clone(),
1399        }
1400    });
1401    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
1402}
1403
1404struct LoadingContext {
1405    id: EntityId,
1406    label: SharedString,
1407    icon: SharedString,
1408    range: Range<Anchor>,
1409    editor: WeakEntity<Editor>,
1410    loading: Option<Task<()>>,
1411    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1412}
1413
1414impl Render for LoadingContext {
1415    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1416        let is_in_text_selection = self
1417            .editor
1418            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
1419            .unwrap_or_default();
1420        ButtonLike::new(("loading-context", self.id))
1421            .style(ButtonStyle::Filled)
1422            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1423            .toggle_state(is_in_text_selection)
1424            .when_some(self.image.clone(), |el, image_task| {
1425                el.hoverable_tooltip(move |_, cx| {
1426                    let image = image_task.peek().cloned().transpose().ok().flatten();
1427                    let image_task = image_task.clone();
1428                    cx.new::<ImageHover>(|cx| ImageHover {
1429                        image,
1430                        _task: cx.spawn(async move |this, cx| {
1431                            if let Ok(image) = image_task.clone().await {
1432                                this.update(cx, |this, cx| {
1433                                    if this.image.replace(image).is_none() {
1434                                        cx.notify();
1435                                    }
1436                                })
1437                                .ok();
1438                            }
1439                        }),
1440                    })
1441                    .into()
1442                })
1443            })
1444            .child(
1445                h_flex()
1446                    .gap_1()
1447                    .child(
1448                        Icon::from_path(self.icon.clone())
1449                            .size(IconSize::XSmall)
1450                            .color(Color::Muted),
1451                    )
1452                    .child(
1453                        Label::new(self.label.clone())
1454                            .size(LabelSize::Small)
1455                            .buffer_font(cx)
1456                            .single_line(),
1457                    )
1458                    .map(|el| {
1459                        if self.loading.is_some() {
1460                            el.with_animation(
1461                                "loading-context-crease",
1462                                Animation::new(Duration::from_secs(2))
1463                                    .repeat()
1464                                    .with_easing(pulsating_between(0.4, 0.8)),
1465                                |label, delta| label.opacity(delta),
1466                            )
1467                            .into_any()
1468                        } else {
1469                            el.into_any()
1470                        }
1471                    }),
1472            )
1473    }
1474}
1475
1476struct ImageHover {
1477    image: Option<Arc<Image>>,
1478    _task: Task<()>,
1479}
1480
1481impl Render for ImageHover {
1482    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1483        if let Some(image) = self.image.clone() {
1484            gpui::img(image).max_w_96().max_h_96().into_any_element()
1485        } else {
1486            gpui::Empty.into_any_element()
1487        }
1488    }
1489}
1490
1491#[derive(Debug, Clone, Eq, PartialEq)]
1492pub enum Mention {
1493    Text {
1494        content: String,
1495        tracked_buffers: Vec<Entity<Buffer>>,
1496    },
1497    Image(MentionImage),
1498    UriOnly,
1499}
1500
1501#[derive(Clone, Debug, Eq, PartialEq)]
1502pub struct MentionImage {
1503    pub data: SharedString,
1504    pub format: ImageFormat,
1505}
1506
1507#[derive(Default)]
1508pub struct MentionSet {
1509    mentions: HashMap<CreaseId, (MentionUri, Shared<Task<Result<Mention, String>>>)>,
1510}
1511
1512impl MentionSet {
1513    fn contents(
1514        &self,
1515        prompt_capabilities: &acp::PromptCapabilities,
1516        full_mention_content: bool,
1517        project: Entity<Project>,
1518        cx: &mut App,
1519    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
1520        if !prompt_capabilities.embedded_context {
1521            let mentions = self
1522                .mentions
1523                .iter()
1524                .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
1525                .collect();
1526
1527            return Task::ready(Ok(mentions));
1528        }
1529
1530        let mentions = self.mentions.clone();
1531        cx.spawn(async move |cx| {
1532            let mut contents = HashMap::default();
1533            for (crease_id, (mention_uri, task)) in mentions {
1534                let content = if full_mention_content
1535                    && let MentionUri::Directory { abs_path } = &mention_uri
1536                {
1537                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
1538                        .await?
1539                } else {
1540                    task.await.map_err(|e| anyhow!("{e}"))?
1541                };
1542
1543                contents.insert(crease_id, (mention_uri, content));
1544            }
1545            Ok(contents)
1546        })
1547    }
1548
1549    fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
1550        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
1551            if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
1552                self.mentions.remove(&crease_id);
1553            }
1554        }
1555    }
1556}
1557
1558pub struct MessageEditorAddon {}
1559
1560impl MessageEditorAddon {
1561    pub fn new() -> Self {
1562        Self {}
1563    }
1564}
1565
1566impl Addon for MessageEditorAddon {
1567    fn to_any(&self) -> &dyn std::any::Any {
1568        self
1569    }
1570
1571    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1572        Some(self)
1573    }
1574
1575    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1576        let settings = agent_settings::AgentSettings::get_global(cx);
1577        if settings.use_modifier_to_send {
1578            key_context.add("use_modifier_to_send");
1579        }
1580    }
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1586
1587    use acp_thread::MentionUri;
1588    use agent_client_protocol as acp;
1589    use agent2::HistoryStore;
1590    use assistant_context::ContextStore;
1591    use assistant_tool::outline;
1592    use editor::{AnchorRangeExt as _, Editor, EditorMode};
1593    use fs::FakeFs;
1594    use futures::StreamExt as _;
1595    use gpui::{
1596        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1597    };
1598    use lsp::{CompletionContext, CompletionTriggerKind};
1599    use project::{CompletionIntent, Project, ProjectPath};
1600    use serde_json::json;
1601    use text::Point;
1602    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1603    use util::{path, paths::PathStyle, rel_path::rel_path, uri};
1604    use workspace::{AppState, Item, Workspace};
1605
1606    use crate::acp::{
1607        message_editor::{Mention, MessageEditor},
1608        thread_view::tests::init_test,
1609    };
1610
1611    #[gpui::test]
1612    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1613        init_test(cx);
1614
1615        let fs = FakeFs::new(cx.executor());
1616        fs.insert_tree("/project", json!({"file": ""})).await;
1617        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1618
1619        let (workspace, cx) =
1620            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1621
1622        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1623        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1624
1625        let message_editor = cx.update(|window, cx| {
1626            cx.new(|cx| {
1627                MessageEditor::new(
1628                    workspace.downgrade(),
1629                    project.clone(),
1630                    history_store.clone(),
1631                    None,
1632                    Default::default(),
1633                    Default::default(),
1634                    "Test Agent".into(),
1635                    "Test",
1636                    EditorMode::AutoHeight {
1637                        min_lines: 1,
1638                        max_lines: None,
1639                    },
1640                    window,
1641                    cx,
1642                )
1643            })
1644        });
1645        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1646
1647        cx.run_until_parked();
1648
1649        let excerpt_id = editor.update(cx, |editor, cx| {
1650            editor
1651                .buffer()
1652                .read(cx)
1653                .excerpt_ids()
1654                .into_iter()
1655                .next()
1656                .unwrap()
1657        });
1658        let completions = editor.update_in(cx, |editor, window, cx| {
1659            editor.set_text("Hello @file ", window, cx);
1660            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1661            let completion_provider = editor.completion_provider().unwrap();
1662            completion_provider.completions(
1663                excerpt_id,
1664                &buffer,
1665                text::Anchor::MAX,
1666                CompletionContext {
1667                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1668                    trigger_character: Some("@".into()),
1669                },
1670                window,
1671                cx,
1672            )
1673        });
1674        let [_, completion]: [_; 2] = completions
1675            .await
1676            .unwrap()
1677            .into_iter()
1678            .flat_map(|response| response.completions)
1679            .collect::<Vec<_>>()
1680            .try_into()
1681            .unwrap();
1682
1683        editor.update_in(cx, |editor, window, cx| {
1684            let snapshot = editor.buffer().read(cx).snapshot(cx);
1685            let start = snapshot
1686                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1687                .unwrap();
1688            let end = snapshot
1689                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1690                .unwrap();
1691            editor.edit([(start..end, completion.new_text)], cx);
1692            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1693        });
1694
1695        cx.run_until_parked();
1696
1697        // Backspace over the inserted crease (and the following space).
1698        editor.update_in(cx, |editor, window, cx| {
1699            editor.backspace(&Default::default(), window, cx);
1700            editor.backspace(&Default::default(), window, cx);
1701        });
1702
1703        let (content, _) = message_editor
1704            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1705            .await
1706            .unwrap();
1707
1708        // We don't send a resource link for the deleted crease.
1709        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1710    }
1711
1712    #[gpui::test]
1713    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1714        init_test(cx);
1715        let fs = FakeFs::new(cx.executor());
1716        fs.insert_tree(
1717            "/test",
1718            json!({
1719                ".zed": {
1720                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1721                },
1722                "src": {
1723                    "main.rs": "fn main() {}",
1724                },
1725            }),
1726        )
1727        .await;
1728
1729        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1730        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1731        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1732        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1733        // Start with no available commands - simulating Claude which doesn't support slash commands
1734        let available_commands = Rc::new(RefCell::new(vec![]));
1735
1736        let (workspace, cx) =
1737            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1738        let workspace_handle = workspace.downgrade();
1739        let message_editor = workspace.update_in(cx, |_, window, cx| {
1740            cx.new(|cx| {
1741                MessageEditor::new(
1742                    workspace_handle.clone(),
1743                    project.clone(),
1744                    history_store.clone(),
1745                    None,
1746                    prompt_capabilities.clone(),
1747                    available_commands.clone(),
1748                    "Claude Code".into(),
1749                    "Test",
1750                    EditorMode::AutoHeight {
1751                        min_lines: 1,
1752                        max_lines: None,
1753                    },
1754                    window,
1755                    cx,
1756                )
1757            })
1758        });
1759        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1760
1761        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1762        editor.update_in(cx, |editor, window, cx| {
1763            editor.set_text("/file test.txt", window, cx);
1764        });
1765
1766        let contents_result = message_editor
1767            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1768            .await;
1769
1770        // Should fail because available_commands is empty (no commands supported)
1771        assert!(contents_result.is_err());
1772        let error_message = contents_result.unwrap_err().to_string();
1773        assert!(error_message.contains("not supported by Claude Code"));
1774        assert!(error_message.contains("Available commands: none"));
1775
1776        // Now simulate Claude providing its list of available commands (which doesn't include file)
1777        available_commands.replace(vec![acp::AvailableCommand {
1778            name: "help".to_string(),
1779            description: "Get help".to_string(),
1780            input: None,
1781            meta: None,
1782        }]);
1783
1784        // Test that unsupported slash commands trigger an error when we have a list of available commands
1785        editor.update_in(cx, |editor, window, cx| {
1786            editor.set_text("/file test.txt", window, cx);
1787        });
1788
1789        let contents_result = message_editor
1790            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1791            .await;
1792
1793        assert!(contents_result.is_err());
1794        let error_message = contents_result.unwrap_err().to_string();
1795        assert!(error_message.contains("not supported by Claude Code"));
1796        assert!(error_message.contains("/file"));
1797        assert!(error_message.contains("Available commands: /help"));
1798
1799        // Test that supported commands work fine
1800        editor.update_in(cx, |editor, window, cx| {
1801            editor.set_text("/help", window, cx);
1802        });
1803
1804        let contents_result = message_editor
1805            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1806            .await;
1807
1808        // Should succeed because /help is in available_commands
1809        assert!(contents_result.is_ok());
1810
1811        // Test that regular text works fine
1812        editor.update_in(cx, |editor, window, cx| {
1813            editor.set_text("Hello Claude!", window, cx);
1814        });
1815
1816        let (content, _) = message_editor
1817            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1818            .await
1819            .unwrap();
1820
1821        assert_eq!(content.len(), 1);
1822        if let acp::ContentBlock::Text(text) = &content[0] {
1823            assert_eq!(text.text, "Hello Claude!");
1824        } else {
1825            panic!("Expected ContentBlock::Text");
1826        }
1827
1828        // Test that @ mentions still work
1829        editor.update_in(cx, |editor, window, cx| {
1830            editor.set_text("Check this @", window, cx);
1831        });
1832
1833        // The @ mention functionality should not be affected
1834        let (content, _) = message_editor
1835            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1836            .await
1837            .unwrap();
1838
1839        assert_eq!(content.len(), 1);
1840        if let acp::ContentBlock::Text(text) = &content[0] {
1841            assert_eq!(text.text, "Check this @");
1842        } else {
1843            panic!("Expected ContentBlock::Text");
1844        }
1845    }
1846
1847    struct MessageEditorItem(Entity<MessageEditor>);
1848
1849    impl Item for MessageEditorItem {
1850        type Event = ();
1851
1852        fn include_in_nav_history() -> bool {
1853            false
1854        }
1855
1856        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1857            "Test".into()
1858        }
1859    }
1860
1861    impl EventEmitter<()> for MessageEditorItem {}
1862
1863    impl Focusable for MessageEditorItem {
1864        fn focus_handle(&self, cx: &App) -> FocusHandle {
1865            self.0.read(cx).focus_handle(cx)
1866        }
1867    }
1868
1869    impl Render for MessageEditorItem {
1870        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1871            self.0.clone().into_any_element()
1872        }
1873    }
1874
1875    #[gpui::test]
1876    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1877        init_test(cx);
1878
1879        let app_state = cx.update(AppState::test);
1880
1881        cx.update(|cx| {
1882            language::init(cx);
1883            editor::init(cx);
1884            workspace::init(app_state.clone(), cx);
1885            Project::init_settings(cx);
1886        });
1887
1888        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1889        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1890        let workspace = window.root(cx).unwrap();
1891
1892        let mut cx = VisualTestContext::from_window(*window, cx);
1893
1894        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1895        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1896        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1897        let available_commands = Rc::new(RefCell::new(vec![
1898            acp::AvailableCommand {
1899                name: "quick-math".to_string(),
1900                description: "2 + 2 = 4 - 1 = 3".to_string(),
1901                input: None,
1902                meta: None,
1903            },
1904            acp::AvailableCommand {
1905                name: "say-hello".to_string(),
1906                description: "Say hello to whoever you want".to_string(),
1907                input: Some(acp::AvailableCommandInput::Unstructured {
1908                    hint: "<name>".to_string(),
1909                }),
1910                meta: None,
1911            },
1912        ]));
1913
1914        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1915            let workspace_handle = cx.weak_entity();
1916            let message_editor = cx.new(|cx| {
1917                MessageEditor::new(
1918                    workspace_handle,
1919                    project.clone(),
1920                    history_store.clone(),
1921                    None,
1922                    prompt_capabilities.clone(),
1923                    available_commands.clone(),
1924                    "Test Agent".into(),
1925                    "Test",
1926                    EditorMode::AutoHeight {
1927                        max_lines: None,
1928                        min_lines: 1,
1929                    },
1930                    window,
1931                    cx,
1932                )
1933            });
1934            workspace.active_pane().update(cx, |pane, cx| {
1935                pane.add_item(
1936                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1937                    true,
1938                    true,
1939                    None,
1940                    window,
1941                    cx,
1942                );
1943            });
1944            message_editor.read(cx).focus_handle(cx).focus(window);
1945            message_editor.read(cx).editor().clone()
1946        });
1947
1948        cx.simulate_input("/");
1949
1950        editor.update_in(&mut cx, |editor, window, cx| {
1951            assert_eq!(editor.text(cx), "/");
1952            assert!(editor.has_visible_completions_menu());
1953
1954            assert_eq!(
1955                current_completion_labels_with_documentation(editor),
1956                &[
1957                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1958                    ("say-hello".into(), "Say hello to whoever you want".into())
1959                ]
1960            );
1961            editor.set_text("", window, cx);
1962        });
1963
1964        cx.simulate_input("/qui");
1965
1966        editor.update_in(&mut cx, |editor, window, cx| {
1967            assert_eq!(editor.text(cx), "/qui");
1968            assert!(editor.has_visible_completions_menu());
1969
1970            assert_eq!(
1971                current_completion_labels_with_documentation(editor),
1972                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1973            );
1974            editor.set_text("", window, cx);
1975        });
1976
1977        editor.update_in(&mut cx, |editor, window, cx| {
1978            assert!(editor.has_visible_completions_menu());
1979            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1980        });
1981
1982        cx.run_until_parked();
1983
1984        editor.update_in(&mut cx, |editor, window, cx| {
1985            assert_eq!(editor.display_text(cx), "/quick-math ");
1986            assert!(!editor.has_visible_completions_menu());
1987            editor.set_text("", window, cx);
1988        });
1989
1990        cx.simulate_input("/say");
1991
1992        editor.update_in(&mut cx, |editor, _window, cx| {
1993            assert_eq!(editor.display_text(cx), "/say");
1994            assert!(editor.has_visible_completions_menu());
1995
1996            assert_eq!(
1997                current_completion_labels_with_documentation(editor),
1998                &[("say-hello".into(), "Say hello to whoever you want".into())]
1999            );
2000        });
2001
2002        editor.update_in(&mut cx, |editor, window, cx| {
2003            assert!(editor.has_visible_completions_menu());
2004            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2005        });
2006
2007        cx.run_until_parked();
2008
2009        editor.update_in(&mut cx, |editor, _window, cx| {
2010            assert_eq!(editor.text(cx), "/say-hello ");
2011            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2012            assert!(editor.has_visible_completions_menu());
2013
2014            assert_eq!(
2015                current_completion_labels_with_documentation(editor),
2016                &[("say-hello".into(), "Say hello to whoever you want".into())]
2017            );
2018        });
2019
2020        cx.simulate_input("GPT5");
2021
2022        editor.update_in(&mut cx, |editor, window, cx| {
2023            assert!(editor.has_visible_completions_menu());
2024            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2025        });
2026
2027        cx.run_until_parked();
2028
2029        editor.update_in(&mut cx, |editor, window, cx| {
2030            assert_eq!(editor.text(cx), "/say-hello GPT5");
2031            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2032            assert!(!editor.has_visible_completions_menu());
2033
2034            // Delete argument
2035            for _ in 0..4 {
2036                editor.backspace(&editor::actions::Backspace, window, cx);
2037            }
2038        });
2039
2040        cx.run_until_parked();
2041
2042        editor.update_in(&mut cx, |editor, window, cx| {
2043            assert_eq!(editor.text(cx), "/say-hello ");
2044            // Hint is visible because argument was deleted
2045            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2046
2047            // Delete last command letter
2048            editor.backspace(&editor::actions::Backspace, window, cx);
2049            editor.backspace(&editor::actions::Backspace, window, cx);
2050        });
2051
2052        cx.run_until_parked();
2053
2054        editor.update_in(&mut cx, |editor, _window, cx| {
2055            // Hint goes away once command no longer matches an available one
2056            assert_eq!(editor.text(cx), "/say-hell");
2057            assert_eq!(editor.display_text(cx), "/say-hell");
2058            assert!(!editor.has_visible_completions_menu());
2059        });
2060    }
2061
2062    #[gpui::test]
2063    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2064        init_test(cx);
2065
2066        let app_state = cx.update(AppState::test);
2067
2068        cx.update(|cx| {
2069            language::init(cx);
2070            editor::init(cx);
2071            workspace::init(app_state.clone(), cx);
2072            Project::init_settings(cx);
2073        });
2074
2075        app_state
2076            .fs
2077            .as_fake()
2078            .insert_tree(
2079                path!("/dir"),
2080                json!({
2081                    "editor": "",
2082                    "a": {
2083                        "one.txt": "1",
2084                        "two.txt": "2",
2085                        "three.txt": "3",
2086                        "four.txt": "4"
2087                    },
2088                    "b": {
2089                        "five.txt": "5",
2090                        "six.txt": "6",
2091                        "seven.txt": "7",
2092                        "eight.txt": "8",
2093                    },
2094                    "x.png": "",
2095                }),
2096            )
2097            .await;
2098
2099        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2100        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2101        let workspace = window.root(cx).unwrap();
2102
2103        let worktree = project.update(cx, |project, cx| {
2104            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2105            assert_eq!(worktrees.len(), 1);
2106            worktrees.pop().unwrap()
2107        });
2108        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2109
2110        let mut cx = VisualTestContext::from_window(*window, cx);
2111
2112        let paths = vec![
2113            rel_path("a/one.txt"),
2114            rel_path("a/two.txt"),
2115            rel_path("a/three.txt"),
2116            rel_path("a/four.txt"),
2117            rel_path("b/five.txt"),
2118            rel_path("b/six.txt"),
2119            rel_path("b/seven.txt"),
2120            rel_path("b/eight.txt"),
2121        ];
2122
2123        let slash = PathStyle::local().separator();
2124
2125        let mut opened_editors = Vec::new();
2126        for path in paths {
2127            let buffer = workspace
2128                .update_in(&mut cx, |workspace, window, cx| {
2129                    workspace.open_path(
2130                        ProjectPath {
2131                            worktree_id,
2132                            path: path.into(),
2133                        },
2134                        None,
2135                        false,
2136                        window,
2137                        cx,
2138                    )
2139                })
2140                .await
2141                .unwrap();
2142            opened_editors.push(buffer);
2143        }
2144
2145        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2146        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2147        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
2148
2149        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2150            let workspace_handle = cx.weak_entity();
2151            let message_editor = cx.new(|cx| {
2152                MessageEditor::new(
2153                    workspace_handle,
2154                    project.clone(),
2155                    history_store.clone(),
2156                    None,
2157                    prompt_capabilities.clone(),
2158                    Default::default(),
2159                    "Test Agent".into(),
2160                    "Test",
2161                    EditorMode::AutoHeight {
2162                        max_lines: None,
2163                        min_lines: 1,
2164                    },
2165                    window,
2166                    cx,
2167                )
2168            });
2169            workspace.active_pane().update(cx, |pane, cx| {
2170                pane.add_item(
2171                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2172                    true,
2173                    true,
2174                    None,
2175                    window,
2176                    cx,
2177                );
2178            });
2179            message_editor.read(cx).focus_handle(cx).focus(window);
2180            let editor = message_editor.read(cx).editor().clone();
2181            (message_editor, editor)
2182        });
2183
2184        cx.simulate_input("Lorem @");
2185
2186        editor.update_in(&mut cx, |editor, window, cx| {
2187            assert_eq!(editor.text(cx), "Lorem @");
2188            assert!(editor.has_visible_completions_menu());
2189
2190            assert_eq!(
2191                current_completion_labels(editor),
2192                &[
2193                    format!("eight.txt dir{slash}b{slash}"),
2194                    format!("seven.txt dir{slash}b{slash}"),
2195                    format!("six.txt dir{slash}b{slash}"),
2196                    format!("five.txt dir{slash}b{slash}"),
2197                ]
2198            );
2199            editor.set_text("", window, cx);
2200        });
2201
2202        prompt_capabilities.replace(acp::PromptCapabilities {
2203            image: true,
2204            audio: true,
2205            embedded_context: true,
2206            meta: None,
2207        });
2208
2209        cx.simulate_input("Lorem ");
2210
2211        editor.update(&mut cx, |editor, cx| {
2212            assert_eq!(editor.text(cx), "Lorem ");
2213            assert!(!editor.has_visible_completions_menu());
2214        });
2215
2216        cx.simulate_input("@");
2217
2218        editor.update(&mut cx, |editor, cx| {
2219            assert_eq!(editor.text(cx), "Lorem @");
2220            assert!(editor.has_visible_completions_menu());
2221            assert_eq!(
2222                current_completion_labels(editor),
2223                &[
2224                    format!("eight.txt dir{slash}b{slash}"),
2225                    format!("seven.txt dir{slash}b{slash}"),
2226                    format!("six.txt dir{slash}b{slash}"),
2227                    format!("five.txt dir{slash}b{slash}"),
2228                    "Files & Directories".into(),
2229                    "Symbols".into(),
2230                    "Threads".into(),
2231                    "Fetch".into()
2232                ]
2233            );
2234        });
2235
2236        // Select and confirm "File"
2237        editor.update_in(&mut cx, |editor, window, cx| {
2238            assert!(editor.has_visible_completions_menu());
2239            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2240            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2241            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2242            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2243            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2244        });
2245
2246        cx.run_until_parked();
2247
2248        editor.update(&mut cx, |editor, cx| {
2249            assert_eq!(editor.text(cx), "Lorem @file ");
2250            assert!(editor.has_visible_completions_menu());
2251        });
2252
2253        cx.simulate_input("one");
2254
2255        editor.update(&mut cx, |editor, cx| {
2256            assert_eq!(editor.text(cx), "Lorem @file one");
2257            assert!(editor.has_visible_completions_menu());
2258            assert_eq!(
2259                current_completion_labels(editor),
2260                vec![format!("one.txt dir{slash}a{slash}")]
2261            );
2262        });
2263
2264        editor.update_in(&mut cx, |editor, window, cx| {
2265            assert!(editor.has_visible_completions_menu());
2266            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2267        });
2268
2269        let url_one = uri!("file:///dir/a/one.txt");
2270        editor.update(&mut cx, |editor, cx| {
2271            let text = editor.text(cx);
2272            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2273            assert!(!editor.has_visible_completions_menu());
2274            assert_eq!(fold_ranges(editor, cx).len(), 1);
2275        });
2276
2277        let all_prompt_capabilities = acp::PromptCapabilities {
2278            image: true,
2279            audio: true,
2280            embedded_context: true,
2281            meta: None,
2282        };
2283
2284        let contents = message_editor
2285            .update(&mut cx, |message_editor, cx| {
2286                message_editor.mention_set().contents(
2287                    &all_prompt_capabilities,
2288                    false,
2289                    project.clone(),
2290                    cx,
2291                )
2292            })
2293            .await
2294            .unwrap()
2295            .into_values()
2296            .collect::<Vec<_>>();
2297
2298        {
2299            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2300                panic!("Unexpected mentions");
2301            };
2302            pretty_assertions::assert_eq!(content, "1");
2303            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
2304        }
2305
2306        let contents = message_editor
2307            .update(&mut cx, |message_editor, cx| {
2308                message_editor.mention_set().contents(
2309                    &acp::PromptCapabilities::default(),
2310                    false,
2311                    project.clone(),
2312                    cx,
2313                )
2314            })
2315            .await
2316            .unwrap()
2317            .into_values()
2318            .collect::<Vec<_>>();
2319
2320        {
2321            let [(uri, Mention::UriOnly)] = contents.as_slice() else {
2322                panic!("Unexpected mentions");
2323            };
2324            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
2325        }
2326
2327        cx.simulate_input(" ");
2328
2329        editor.update(&mut cx, |editor, cx| {
2330            let text = editor.text(cx);
2331            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2332            assert!(!editor.has_visible_completions_menu());
2333            assert_eq!(fold_ranges(editor, cx).len(), 1);
2334        });
2335
2336        cx.simulate_input("Ipsum ");
2337
2338        editor.update(&mut cx, |editor, cx| {
2339            let text = editor.text(cx);
2340            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2341            assert!(!editor.has_visible_completions_menu());
2342            assert_eq!(fold_ranges(editor, cx).len(), 1);
2343        });
2344
2345        cx.simulate_input("@file ");
2346
2347        editor.update(&mut cx, |editor, cx| {
2348            let text = editor.text(cx);
2349            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2350            assert!(editor.has_visible_completions_menu());
2351            assert_eq!(fold_ranges(editor, cx).len(), 1);
2352        });
2353
2354        editor.update_in(&mut cx, |editor, window, cx| {
2355            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2356        });
2357
2358        cx.run_until_parked();
2359
2360        let contents = message_editor
2361            .update(&mut cx, |message_editor, cx| {
2362                message_editor.mention_set().contents(
2363                    &all_prompt_capabilities,
2364                    false,
2365                    project.clone(),
2366                    cx,
2367                )
2368            })
2369            .await
2370            .unwrap()
2371            .into_values()
2372            .collect::<Vec<_>>();
2373
2374        let url_eight = uri!("file:///dir/b/eight.txt");
2375
2376        {
2377            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2378                panic!("Unexpected mentions");
2379            };
2380            pretty_assertions::assert_eq!(content, "8");
2381            pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
2382        }
2383
2384        editor.update(&mut cx, |editor, cx| {
2385            assert_eq!(
2386                editor.text(cx),
2387                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2388            );
2389            assert!(!editor.has_visible_completions_menu());
2390            assert_eq!(fold_ranges(editor, cx).len(), 2);
2391        });
2392
2393        let plain_text_language = Arc::new(language::Language::new(
2394            language::LanguageConfig {
2395                name: "Plain Text".into(),
2396                matcher: language::LanguageMatcher {
2397                    path_suffixes: vec!["txt".to_string()],
2398                    ..Default::default()
2399                },
2400                ..Default::default()
2401            },
2402            None,
2403        ));
2404
2405        // Register the language and fake LSP
2406        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2407        language_registry.add(plain_text_language);
2408
2409        let mut fake_language_servers = language_registry.register_fake_lsp(
2410            "Plain Text",
2411            language::FakeLspAdapter {
2412                capabilities: lsp::ServerCapabilities {
2413                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2414                    ..Default::default()
2415                },
2416                ..Default::default()
2417            },
2418        );
2419
2420        // Open the buffer to trigger LSP initialization
2421        let buffer = project
2422            .update(&mut cx, |project, cx| {
2423                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2424            })
2425            .await
2426            .unwrap();
2427
2428        // Register the buffer with language servers
2429        let _handle = project.update(&mut cx, |project, cx| {
2430            project.register_buffer_with_language_servers(&buffer, cx)
2431        });
2432
2433        cx.run_until_parked();
2434
2435        let fake_language_server = fake_language_servers.next().await.unwrap();
2436        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2437            move |_, _| async move {
2438                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2439                    #[allow(deprecated)]
2440                    lsp::SymbolInformation {
2441                        name: "MySymbol".into(),
2442                        location: lsp::Location {
2443                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2444                            range: lsp::Range::new(
2445                                lsp::Position::new(0, 0),
2446                                lsp::Position::new(0, 1),
2447                            ),
2448                        },
2449                        kind: lsp::SymbolKind::CONSTANT,
2450                        tags: None,
2451                        container_name: None,
2452                        deprecated: None,
2453                    },
2454                ])))
2455            },
2456        );
2457
2458        cx.simulate_input("@symbol ");
2459
2460        editor.update(&mut cx, |editor, cx| {
2461            assert_eq!(
2462                editor.text(cx),
2463                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2464            );
2465            assert!(editor.has_visible_completions_menu());
2466            assert_eq!(current_completion_labels(editor), &["MySymbol"]);
2467        });
2468
2469        editor.update_in(&mut cx, |editor, window, cx| {
2470            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2471        });
2472
2473        let contents = message_editor
2474            .update(&mut cx, |message_editor, cx| {
2475                message_editor.mention_set().contents(
2476                    &all_prompt_capabilities,
2477                    false,
2478                    project.clone(),
2479                    cx,
2480                )
2481            })
2482            .await
2483            .unwrap()
2484            .into_values()
2485            .collect::<Vec<_>>();
2486
2487        {
2488            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2489                panic!("Unexpected mentions");
2490            };
2491            pretty_assertions::assert_eq!(content, "1");
2492            pretty_assertions::assert_eq!(
2493                uri,
2494                &format!("{url_one}?symbol=MySymbol#L1:1")
2495                    .parse::<MentionUri>()
2496                    .unwrap()
2497            );
2498        }
2499
2500        cx.run_until_parked();
2501
2502        editor.read_with(&cx, |editor, cx| {
2503            assert_eq!(
2504                editor.text(cx),
2505                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2506            );
2507        });
2508
2509        // Try to mention an "image" file that will fail to load
2510        cx.simulate_input("@file x.png");
2511
2512        editor.update(&mut cx, |editor, cx| {
2513            assert_eq!(
2514                editor.text(cx),
2515                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
2516            );
2517            assert!(editor.has_visible_completions_menu());
2518            assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
2519        });
2520
2521        editor.update_in(&mut cx, |editor, window, cx| {
2522            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2523        });
2524
2525        // Getting the message contents fails
2526        message_editor
2527            .update(&mut cx, |message_editor, cx| {
2528                message_editor.mention_set().contents(
2529                    &all_prompt_capabilities,
2530                    false,
2531                    project.clone(),
2532                    cx,
2533                )
2534            })
2535            .await
2536            .expect_err("Should fail to load x.png");
2537
2538        cx.run_until_parked();
2539
2540        // Mention was removed
2541        editor.read_with(&cx, |editor, cx| {
2542            assert_eq!(
2543                editor.text(cx),
2544                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2545            );
2546        });
2547
2548        // Once more
2549        cx.simulate_input("@file x.png");
2550
2551        editor.update(&mut cx, |editor, cx| {
2552                    assert_eq!(
2553                        editor.text(cx),
2554                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
2555                    );
2556                    assert!(editor.has_visible_completions_menu());
2557                    assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
2558                });
2559
2560        editor.update_in(&mut cx, |editor, window, cx| {
2561            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2562        });
2563
2564        // This time don't immediately get the contents, just let the confirmed completion settle
2565        cx.run_until_parked();
2566
2567        // Mention was removed
2568        editor.read_with(&cx, |editor, cx| {
2569                    assert_eq!(
2570                        editor.text(cx),
2571                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2572                    );
2573                });
2574
2575        // Now getting the contents succeeds, because the invalid mention was removed
2576        let contents = message_editor
2577            .update(&mut cx, |message_editor, cx| {
2578                message_editor.mention_set().contents(
2579                    &all_prompt_capabilities,
2580                    false,
2581                    project.clone(),
2582                    cx,
2583                )
2584            })
2585            .await
2586            .unwrap();
2587        assert_eq!(contents.len(), 3);
2588    }
2589
2590    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2591        let snapshot = editor.buffer().read(cx).snapshot(cx);
2592        editor.display_map.update(cx, |display_map, cx| {
2593            display_map
2594                .snapshot(cx)
2595                .folds_in_range(0..snapshot.len())
2596                .map(|fold| fold.range.to_point(&snapshot))
2597                .collect()
2598        })
2599    }
2600
2601    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2602        let completions = editor.current_completions().expect("Missing completions");
2603        completions
2604            .into_iter()
2605            .map(|completion| completion.label.text)
2606            .collect::<Vec<_>>()
2607    }
2608
2609    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2610        let completions = editor.current_completions().expect("Missing completions");
2611        completions
2612            .into_iter()
2613            .map(|completion| {
2614                (
2615                    completion.label.text,
2616                    completion
2617                        .documentation
2618                        .map(|d| d.text().to_string())
2619                        .unwrap_or_default(),
2620                )
2621            })
2622            .collect::<Vec<_>>()
2623    }
2624
2625    #[gpui::test]
2626    async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
2627        init_test(cx);
2628
2629        let fs = FakeFs::new(cx.executor());
2630
2631        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2632        const LINE: &str = "fn example_function() { /* some code */ }\n";
2633        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2634        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2635
2636        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2637        let small_content = "fn small_function() { /* small */ }\n";
2638        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2639
2640        fs.insert_tree(
2641            "/project",
2642            json!({
2643                "large_file.rs": large_content.clone(),
2644                "small_file.rs": small_content,
2645            }),
2646        )
2647        .await;
2648
2649        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2650
2651        let (workspace, cx) =
2652            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2653
2654        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2655        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2656
2657        let message_editor = cx.update(|window, cx| {
2658            cx.new(|cx| {
2659                let editor = MessageEditor::new(
2660                    workspace.downgrade(),
2661                    project.clone(),
2662                    history_store.clone(),
2663                    None,
2664                    Default::default(),
2665                    Default::default(),
2666                    "Test Agent".into(),
2667                    "Test",
2668                    EditorMode::AutoHeight {
2669                        min_lines: 1,
2670                        max_lines: None,
2671                    },
2672                    window,
2673                    cx,
2674                );
2675                // Enable embedded context so files are actually included
2676                editor.prompt_capabilities.replace(acp::PromptCapabilities {
2677                    embedded_context: true,
2678                    meta: None,
2679                    ..Default::default()
2680                });
2681                editor
2682            })
2683        });
2684
2685        // Test large file mention
2686        // Get the absolute path using the project's worktree
2687        let large_file_abs_path = project.read_with(cx, |project, cx| {
2688            let worktree = project.worktrees(cx).next().unwrap();
2689            let worktree_root = worktree.read(cx).abs_path();
2690            worktree_root.join("large_file.rs")
2691        });
2692        let large_file_task = message_editor.update(cx, |editor, cx| {
2693            editor.confirm_mention_for_file(large_file_abs_path, cx)
2694        });
2695
2696        let large_file_mention = large_file_task.await.unwrap();
2697        match large_file_mention {
2698            Mention::Text { content, .. } => {
2699                // Should contain outline header for large files
2700                assert!(content.contains("File outline for"));
2701                assert!(content.contains("file too large to show full content"));
2702                // Should not contain the full repeated content
2703                assert!(!content.contains(&LINE.repeat(100)));
2704            }
2705            _ => panic!("Expected Text mention for large file"),
2706        }
2707
2708        // Test small file mention
2709        // Get the absolute path using the project's worktree
2710        let small_file_abs_path = project.read_with(cx, |project, cx| {
2711            let worktree = project.worktrees(cx).next().unwrap();
2712            let worktree_root = worktree.read(cx).abs_path();
2713            worktree_root.join("small_file.rs")
2714        });
2715        let small_file_task = message_editor.update(cx, |editor, cx| {
2716            editor.confirm_mention_for_file(small_file_abs_path, cx)
2717        });
2718
2719        let small_file_mention = small_file_task.await.unwrap();
2720        match small_file_mention {
2721            Mention::Text { content, .. } => {
2722                // Should contain the actual content
2723                assert_eq!(content, small_content);
2724                // Should not contain outline header
2725                assert!(!content.contains("File outline for"));
2726            }
2727            _ => panic!("Expected Text mention for small file"),
2728        }
2729    }
2730}