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