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