message_editor.rs

   1use crate::DEFAULT_THREAD_TITLE;
   2use crate::SendImmediately;
   3use crate::ThreadHistory;
   4use crate::{
   5    ChatWithFollow,
   6    completion_provider::{
   7        PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,
   8        PromptContextType, SlashCommandCompletion,
   9    },
  10    mention_set::{Mention, MentionImage, MentionSet, insert_crease_for_mention},
  11};
  12use acp_thread::MentionUri;
  13use agent::ThreadStore;
  14use agent_client_protocol as acp;
  15use anyhow::{Result, anyhow};
  16use editor::{
  17    Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,
  18    EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,
  19    actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
  20};
  21use futures::{FutureExt as _, future::join_all};
  22use gpui::{
  23    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
  24    KeyContext, SharedString, Subscription, Task, TaskExt, TextStyle, WeakEntity,
  25};
  26use language::{Buffer, language_settings::InlayHintKind};
  27use parking_lot::RwLock;
  28use project::AgentId;
  29use project::{
  30    CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
  31};
  32use prompt_store::PromptStore;
  33use rope::Point;
  34use settings::Settings;
  35use std::{fmt::Write, ops::Range, rc::Rc, sync::Arc};
  36use theme_settings::ThemeSettings;
  37use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};
  38use util::paths::PathStyle;
  39use util::{ResultExt, debug_panic};
  40use workspace::{CollaboratorId, Workspace};
  41use zed_actions::agent::{Chat, PasteRaw};
  42
  43#[derive(Default)]
  44pub struct SessionCapabilities {
  45    prompt_capabilities: acp::PromptCapabilities,
  46    available_commands: Vec<acp::AvailableCommand>,
  47}
  48
  49impl SessionCapabilities {
  50    pub fn new(
  51        prompt_capabilities: acp::PromptCapabilities,
  52        available_commands: Vec<acp::AvailableCommand>,
  53    ) -> Self {
  54        Self {
  55            prompt_capabilities,
  56            available_commands,
  57        }
  58    }
  59
  60    pub fn supports_images(&self) -> bool {
  61        self.prompt_capabilities.image
  62    }
  63
  64    pub fn supports_embedded_context(&self) -> bool {
  65        self.prompt_capabilities.embedded_context
  66    }
  67
  68    pub fn available_commands(&self) -> &[acp::AvailableCommand] {
  69        &self.available_commands
  70    }
  71
  72    fn supported_modes(&self, has_thread_store: bool) -> Vec<PromptContextType> {
  73        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
  74        if self.prompt_capabilities.embedded_context {
  75            if has_thread_store {
  76                supported.push(PromptContextType::Thread);
  77            }
  78            supported.extend(&[
  79                PromptContextType::Diagnostics,
  80                PromptContextType::Fetch,
  81                PromptContextType::Rules,
  82                PromptContextType::BranchDiff,
  83            ]);
  84        }
  85        supported
  86    }
  87
  88    pub fn completion_commands(&self) -> Vec<crate::completion_provider::AvailableCommand> {
  89        self.available_commands
  90            .iter()
  91            .map(|cmd| crate::completion_provider::AvailableCommand {
  92                name: cmd.name.clone().into(),
  93                description: cmd.description.clone().into(),
  94                requires_argument: cmd.input.is_some(),
  95            })
  96            .collect()
  97    }
  98
  99    pub fn set_prompt_capabilities(&mut self, prompt_capabilities: acp::PromptCapabilities) {
 100        self.prompt_capabilities = prompt_capabilities;
 101    }
 102
 103    pub fn set_available_commands(&mut self, available_commands: Vec<acp::AvailableCommand>) {
 104        self.available_commands = available_commands;
 105    }
 106}
 107
 108pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>;
 109
 110struct MessageEditorCompletionDelegate {
 111    session_capabilities: SharedSessionCapabilities,
 112    has_thread_store: bool,
 113    message_editor: WeakEntity<MessageEditor>,
 114}
 115
 116impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate {
 117    fn supports_images(&self, _cx: &App) -> bool {
 118        self.session_capabilities.read().supports_images()
 119    }
 120
 121    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
 122        self.session_capabilities
 123            .read()
 124            .supported_modes(self.has_thread_store)
 125    }
 126
 127    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
 128        self.session_capabilities.read().completion_commands()
 129    }
 130
 131    fn confirm_command(&self, cx: &mut App) {
 132        let _ = self.message_editor.update(cx, |this, cx| this.send(cx));
 133    }
 134}
 135
 136pub struct MessageEditor {
 137    mention_set: Entity<MentionSet>,
 138    editor: Entity<Editor>,
 139    workspace: WeakEntity<Workspace>,
 140    session_capabilities: SharedSessionCapabilities,
 141    agent_id: AgentId,
 142    thread_store: Option<Entity<ThreadStore>>,
 143    _subscriptions: Vec<Subscription>,
 144    _parse_slash_command_task: Task<()>,
 145}
 146
 147#[derive(Clone, Debug)]
 148pub enum MessageEditorEvent {
 149    Send,
 150    SendImmediately,
 151    Cancel,
 152    Focus,
 153    LostFocus,
 154    InputAttempted {
 155        text: Arc<str>,
 156        cursor_offset: usize,
 157    },
 158}
 159
 160impl EventEmitter<MessageEditorEvent> for MessageEditor {}
 161
 162const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
 163
 164enum MentionInsertPosition {
 165    AtCursor,
 166    EndOfBuffer,
 167}
 168
 169fn insert_mention_for_project_path(
 170    project_path: &ProjectPath,
 171    position: MentionInsertPosition,
 172    editor: &Entity<Editor>,
 173    mention_set: &Entity<MentionSet>,
 174    project: &Entity<Project>,
 175    workspace: &Entity<Workspace>,
 176    supports_images: bool,
 177    window: &mut Window,
 178    cx: &mut App,
 179) -> Option<Task<()>> {
 180    let (file_name, mention_uri) = {
 181        let project = project.read(cx);
 182        let path_style = project.path_style(cx);
 183        let entry = project.entry_for_path(project_path, cx)?;
 184        let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
 185        let abs_path = worktree.read(cx).absolutize(&project_path.path);
 186        let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 187            &project_path.path,
 188            worktree.read(cx).root_name(),
 189            path_style,
 190        );
 191        let mention_uri = if entry.is_dir() {
 192            MentionUri::Directory { abs_path }
 193        } else {
 194            MentionUri::File { abs_path }
 195        };
 196        (file_name, mention_uri)
 197    };
 198
 199    let mention_text = mention_uri.as_link().to_string();
 200    let content_len = mention_text.len();
 201
 202    let text_anchor = match position {
 203        MentionInsertPosition::AtCursor => editor.update(cx, |editor, cx| {
 204            let buffer = editor.buffer().read(cx);
 205            let snapshot = buffer.snapshot(cx);
 206            let buffer_snapshot = snapshot.as_singleton()?;
 207            let text_anchor = snapshot
 208                .anchor_to_buffer_anchor(editor.selections.newest_anchor().start)?
 209                .0
 210                .bias_left(&buffer_snapshot);
 211
 212            editor.insert(&mention_text, window, cx);
 213            editor.insert(" ", window, cx);
 214
 215            Some(text_anchor)
 216        }),
 217        MentionInsertPosition::EndOfBuffer => {
 218            let multi_buffer = editor.read(cx).buffer().clone();
 219            let buffer = multi_buffer.read(cx).as_singleton()?;
 220            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 221            let new_text = format!("{mention_text} ");
 222            editor.update(cx, |editor, cx| {
 223                editor.edit(
 224                    [(
 225                        multi_buffer::Anchor::Max..multi_buffer::Anchor::Max,
 226                        new_text,
 227                    )],
 228                    cx,
 229                );
 230            });
 231            Some(anchor)
 232        }
 233    }?;
 234
 235    Some(mention_set.update(cx, |mention_set, cx| {
 236        mention_set.confirm_mention_completion(
 237            file_name,
 238            text_anchor,
 239            content_len,
 240            mention_uri,
 241            supports_images,
 242            editor.clone(),
 243            workspace,
 244            window,
 245            cx,
 246        )
 247    }))
 248}
 249
 250enum ResolvedPastedContextItem {
 251    Image(gpui::Image, gpui::SharedString),
 252    ProjectPath(ProjectPath),
 253}
 254
 255async fn resolve_pasted_context_items(
 256    project: Entity<Project>,
 257    project_is_local: bool,
 258    supports_images: bool,
 259    entries: Vec<ClipboardEntry>,
 260    cx: &mut gpui::AsyncWindowContext,
 261) -> (Vec<ResolvedPastedContextItem>, Vec<Entity<Worktree>>) {
 262    let mut items = Vec::new();
 263    let mut added_worktrees = Vec::new();
 264    let default_image_name: SharedString = "Image".into();
 265
 266    for entry in entries {
 267        match entry {
 268            ClipboardEntry::String(_) => {}
 269            ClipboardEntry::Image(image) => {
 270                if supports_images {
 271                    items.push(ResolvedPastedContextItem::Image(
 272                        image,
 273                        default_image_name.clone(),
 274                    ));
 275                }
 276            }
 277            ClipboardEntry::ExternalPaths(paths) => {
 278                for path in paths.paths().iter() {
 279                    if let Some((image, name)) = cx
 280                        .background_spawn({
 281                            let path = path.clone();
 282                            let default_image_name = default_image_name.clone();
 283                            async move {
 284                                crate::mention_set::load_external_image_from_path(
 285                                    &path,
 286                                    &default_image_name,
 287                                )
 288                            }
 289                        })
 290                        .await
 291                    {
 292                        if supports_images {
 293                            items.push(ResolvedPastedContextItem::Image(image, name));
 294                        }
 295                        continue;
 296                    }
 297
 298                    if !project_is_local {
 299                        continue;
 300                    }
 301
 302                    let path = path.clone();
 303                    let Ok(resolve_task) = cx.update({
 304                        let project = project.clone();
 305                        move |_, cx| Workspace::project_path_for_path(project, &path, false, cx)
 306                    }) else {
 307                        continue;
 308                    };
 309
 310                    if let Some((worktree, project_path)) = resolve_task.await.log_err() {
 311                        added_worktrees.push(worktree);
 312                        items.push(ResolvedPastedContextItem::ProjectPath(project_path));
 313                    }
 314                }
 315            }
 316        }
 317    }
 318
 319    (items, added_worktrees)
 320}
 321
 322fn insert_project_path_as_context(
 323    project_path: ProjectPath,
 324    editor: Entity<Editor>,
 325    mention_set: Entity<MentionSet>,
 326    workspace: WeakEntity<Workspace>,
 327    supports_images: bool,
 328    cx: &mut gpui::AsyncWindowContext,
 329) -> Option<Task<()>> {
 330    let workspace = workspace.upgrade()?;
 331
 332    cx.update(move |window, cx| {
 333        let project = workspace.read(cx).project().clone();
 334        insert_mention_for_project_path(
 335            &project_path,
 336            MentionInsertPosition::AtCursor,
 337            &editor,
 338            &mention_set,
 339            &project,
 340            &workspace,
 341            supports_images,
 342            window,
 343            cx,
 344        )
 345    })
 346    .ok()
 347    .flatten()
 348}
 349
 350async fn insert_resolved_pasted_context_items(
 351    items: Vec<ResolvedPastedContextItem>,
 352    added_worktrees: Vec<Entity<Worktree>>,
 353    editor: Entity<Editor>,
 354    mention_set: Entity<MentionSet>,
 355    workspace: WeakEntity<Workspace>,
 356    supports_images: bool,
 357    cx: &mut gpui::AsyncWindowContext,
 358) {
 359    let mut path_mention_tasks = Vec::new();
 360
 361    for item in items {
 362        match item {
 363            ResolvedPastedContextItem::Image(image, name) => {
 364                crate::mention_set::insert_images_as_context(
 365                    vec![(image, name)],
 366                    editor.clone(),
 367                    mention_set.clone(),
 368                    workspace.clone(),
 369                    cx,
 370                )
 371                .await;
 372            }
 373            ResolvedPastedContextItem::ProjectPath(project_path) => {
 374                if let Some(task) = insert_project_path_as_context(
 375                    project_path,
 376                    editor.clone(),
 377                    mention_set.clone(),
 378                    workspace.clone(),
 379                    supports_images,
 380                    cx,
 381                ) {
 382                    path_mention_tasks.push(task);
 383                }
 384            }
 385        }
 386    }
 387
 388    join_all(path_mention_tasks).await;
 389    drop(added_worktrees);
 390}
 391
 392impl MessageEditor {
 393    pub fn new(
 394        workspace: WeakEntity<Workspace>,
 395        project: WeakEntity<Project>,
 396        thread_store: Option<Entity<ThreadStore>>,
 397        history: Option<WeakEntity<ThreadHistory>>,
 398        prompt_store: Option<Entity<PromptStore>>,
 399        session_capabilities: SharedSessionCapabilities,
 400        agent_id: AgentId,
 401        placeholder: &str,
 402        mode: EditorMode,
 403        window: &mut Window,
 404        cx: &mut Context<Self>,
 405    ) -> Self {
 406        let language_registry = project
 407            .upgrade()
 408            .map(|project| project.read(cx).languages().clone());
 409
 410        let editor = cx.new(|cx| {
 411            let buffer = cx.new(|cx| {
 412                let buffer = Buffer::local("", cx);
 413                if let Some(language_registry) = language_registry.as_ref() {
 414                    buffer.set_language_registry(language_registry.clone());
 415                }
 416                buffer
 417            });
 418            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 419
 420            let mut editor = Editor::new(mode, buffer, None, window, cx);
 421            editor.set_placeholder_text(placeholder, window, cx);
 422            editor.set_show_indent_guides(false, cx);
 423            editor.set_show_completions_on_input(Some(true));
 424            editor.set_soft_wrap();
 425            editor.disable_mouse_wheel_zoom();
 426            editor.set_use_modal_editing(true);
 427            editor.set_context_menu_options(ContextMenuOptions {
 428                min_entries_visible: 12,
 429                max_entries_visible: 12,
 430                placement: None,
 431            });
 432            editor.register_addon(MessageEditorAddon::new());
 433
 434            editor.set_custom_context_menu(|editor, _point, window, cx| {
 435                let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
 436
 437                Some(ContextMenu::build(window, cx, |menu, _, _| {
 438                    menu.action("Cut", Box::new(editor::actions::Cut))
 439                        .action_disabled_when(
 440                            !has_selection,
 441                            "Copy",
 442                            Box::new(editor::actions::Copy),
 443                        )
 444                        .action("Paste", Box::new(editor::actions::Paste))
 445                        .action("Paste as Plain Text", Box::new(PasteRaw))
 446                }))
 447            });
 448
 449            editor
 450        });
 451        let mention_set =
 452            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
 453        let completion_provider = Rc::new(PromptCompletionProvider::new(
 454            MessageEditorCompletionDelegate {
 455                session_capabilities: session_capabilities.clone(),
 456                has_thread_store: thread_store.is_some(),
 457                message_editor: cx.weak_entity(),
 458            },
 459            editor.downgrade(),
 460            mention_set.clone(),
 461            history,
 462            prompt_store.clone(),
 463            workspace.clone(),
 464        ));
 465        editor.update(cx, |editor, _cx| {
 466            editor.set_completion_provider(Some(completion_provider.clone()))
 467        });
 468
 469        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 470            cx.emit(MessageEditorEvent::Focus)
 471        })
 472        .detach();
 473        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 474            cx.emit(MessageEditorEvent::LostFocus)
 475        })
 476        .detach();
 477
 478        let mut has_hint = false;
 479        let mut subscriptions = Vec::new();
 480
 481        subscriptions.push(cx.subscribe_in(&editor, window, {
 482            move |this, editor, event, window, cx| {
 483                let input_attempted_text = match event {
 484                    EditorEvent::InputHandled { text, .. } => Some(text),
 485                    EditorEvent::InputIgnored { text } => Some(text),
 486                    _ => None,
 487                };
 488                if let Some(text) = input_attempted_text
 489                    && editor.read(cx).read_only(cx)
 490                    && !text.is_empty()
 491                {
 492                    let editor = editor.read(cx);
 493                    let cursor_anchor = editor.selections.newest_anchor().head();
 494                    let cursor_offset = cursor_anchor
 495                        .to_offset(&editor.buffer().read(cx).snapshot(cx))
 496                        .0;
 497                    cx.emit(MessageEditorEvent::InputAttempted {
 498                        text: text.clone(),
 499                        cursor_offset,
 500                    });
 501                }
 502
 503                if let EditorEvent::Edited { .. } = event
 504                    && !editor.read(cx).read_only(cx)
 505                {
 506                    editor.update(cx, |editor, cx| {
 507                        let snapshot = editor.snapshot(window, cx);
 508                        this.mention_set
 509                            .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 510
 511                        let new_hints = this
 512                            .command_hint(snapshot.buffer())
 513                            .into_iter()
 514                            .collect::<Vec<_>>();
 515                        let has_new_hint = !new_hints.is_empty();
 516                        editor.splice_inlays(
 517                            if has_hint {
 518                                &[COMMAND_HINT_INLAY_ID]
 519                            } else {
 520                                &[]
 521                            },
 522                            new_hints,
 523                            cx,
 524                        );
 525                        has_hint = has_new_hint;
 526                    });
 527                    cx.notify();
 528                }
 529            }
 530        }));
 531
 532        if let Some(language_registry) = language_registry {
 533            let editor = editor.clone();
 534            cx.spawn(async move |_, cx| {
 535                let markdown = language_registry.language_for_name("Markdown").await?;
 536                editor.update(cx, |editor, cx| {
 537                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 538                        buffer.update(cx, |buffer, cx| {
 539                            buffer.set_language(Some(markdown), cx);
 540                        });
 541                    }
 542                });
 543                anyhow::Ok(())
 544            })
 545            .detach_and_log_err(cx);
 546        }
 547
 548        Self {
 549            editor,
 550            mention_set,
 551            workspace,
 552            session_capabilities,
 553            agent_id,
 554            thread_store,
 555            _subscriptions: subscriptions,
 556            _parse_slash_command_task: Task::ready(()),
 557        }
 558    }
 559
 560    pub fn set_session_capabilities(
 561        &mut self,
 562        session_capabilities: SharedSessionCapabilities,
 563        _cx: &mut Context<Self>,
 564    ) {
 565        self.session_capabilities = session_capabilities;
 566    }
 567
 568    fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
 569        let session_capabilities = self.session_capabilities.read();
 570        let available_commands = session_capabilities.available_commands();
 571        if available_commands.is_empty() {
 572            return None;
 573        }
 574
 575        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 576        if parsed_command.argument.is_some() {
 577            return None;
 578        }
 579
 580        let command_name = parsed_command.command?;
 581        let available_command = available_commands
 582            .iter()
 583            .find(|command| command.name == command_name)?;
 584
 585        let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
 586            mut hint,
 587            ..
 588        }) = available_command.input.clone()?
 589        else {
 590            return None;
 591        };
 592
 593        let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
 594        if hint_pos > snapshot.len() {
 595            hint_pos = snapshot.len();
 596            hint.insert(0, ' ');
 597        }
 598
 599        let hint_pos = snapshot.anchor_after(hint_pos);
 600
 601        Some(Inlay::hint(
 602            COMMAND_HINT_INLAY_ID,
 603            hint_pos,
 604            &InlayHint {
 605                position: snapshot.anchor_to_buffer_anchor(hint_pos)?.0,
 606                label: InlayHintLabel::String(hint),
 607                kind: Some(InlayHintKind::Parameter),
 608                padding_left: false,
 609                padding_right: false,
 610                tooltip: None,
 611                resolve_state: project::ResolveState::Resolved,
 612            },
 613        ))
 614    }
 615
 616    pub fn insert_thread_summary(
 617        &mut self,
 618        session_id: acp::SessionId,
 619        title: Option<SharedString>,
 620        window: &mut Window,
 621        cx: &mut Context<Self>,
 622    ) {
 623        if self.thread_store.is_none() {
 624            return;
 625        }
 626        let Some(workspace) = self.workspace.upgrade() else {
 627            return;
 628        };
 629        let thread_title = title
 630            .filter(|title| !title.is_empty())
 631            .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
 632        let uri = MentionUri::Thread {
 633            id: session_id,
 634            name: thread_title.to_string(),
 635        };
 636        let content = format!("{}\n", uri.as_link());
 637
 638        let content_len = content.len() - 1;
 639
 640        let start = self.editor.update(cx, |editor, cx| {
 641            editor.set_text(content, window, cx);
 642            let snapshot = editor.buffer().read(cx).snapshot(cx);
 643            snapshot
 644                .anchor_to_buffer_anchor(snapshot.anchor_before(Point::zero()))
 645                .unwrap()
 646                .0
 647        });
 648
 649        let supports_images = self.session_capabilities.read().supports_images();
 650
 651        self.mention_set
 652            .update(cx, |mention_set, cx| {
 653                mention_set.confirm_mention_completion(
 654                    thread_title,
 655                    start,
 656                    content_len,
 657                    uri,
 658                    supports_images,
 659                    self.editor.clone(),
 660                    &workspace,
 661                    window,
 662                    cx,
 663                )
 664            })
 665            .detach();
 666    }
 667
 668    #[cfg(test)]
 669    pub(crate) fn editor(&self) -> &Entity<Editor> {
 670        &self.editor
 671    }
 672
 673    pub fn is_empty(&self, cx: &App) -> bool {
 674        self.editor.read(cx).is_empty(cx)
 675    }
 676
 677    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 678        self.editor
 679            .read(cx)
 680            .context_menu()
 681            .borrow()
 682            .as_ref()
 683            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 684    }
 685
 686    #[cfg(test)]
 687    pub fn mention_set(&self) -> &Entity<MentionSet> {
 688        &self.mention_set
 689    }
 690
 691    fn validate_slash_commands(
 692        text: &str,
 693        available_commands: &[acp::AvailableCommand],
 694        agent_id: &AgentId,
 695    ) -> Result<()> {
 696        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 697            if let Some(command_name) = parsed_command.command {
 698                // Check if this command is in the list of available commands from the server
 699                let is_supported = available_commands
 700                    .iter()
 701                    .any(|cmd| cmd.name == command_name);
 702
 703                if !is_supported {
 704                    return Err(anyhow!(
 705                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 706                        command_name,
 707                        agent_id,
 708                        if available_commands.is_empty() {
 709                            "none".to_string()
 710                        } else {
 711                            available_commands
 712                                .iter()
 713                                .map(|cmd| format!("/{}", cmd.name))
 714                                .collect::<Vec<_>>()
 715                                .join(", ")
 716                        }
 717                    ));
 718                }
 719            }
 720        }
 721        Ok(())
 722    }
 723
 724    pub fn contents(
 725        &self,
 726        full_mention_content: bool,
 727        cx: &mut Context<Self>,
 728    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 729        let text = self.editor.read(cx).text(cx);
 730        let available_commands = self
 731            .session_capabilities
 732            .read()
 733            .available_commands()
 734            .to_vec();
 735        let agent_id = self.agent_id.clone();
 736        let build_task = self.build_content_blocks(full_mention_content, cx);
 737
 738        cx.spawn(async move |_, _cx| {
 739            Self::validate_slash_commands(&text, &available_commands, &agent_id)?;
 740            build_task.await
 741        })
 742    }
 743
 744    pub fn draft_contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> {
 745        let build_task = self.build_content_blocks(false, cx);
 746        cx.spawn(async move |_, _cx| {
 747            let (blocks, _tracked_buffers) = build_task.await?;
 748            Ok(blocks)
 749        })
 750    }
 751
 752    fn build_content_blocks(
 753        &self,
 754        full_mention_content: bool,
 755        cx: &mut Context<Self>,
 756    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 757        let contents = self
 758            .mention_set
 759            .update(cx, |store, cx| store.contents(full_mention_content, cx));
 760        let editor = self.editor.clone();
 761        let supports_embedded_context =
 762            self.session_capabilities.read().supports_embedded_context();
 763
 764        cx.spawn(async move |_, cx| {
 765            let contents = contents.await?;
 766            let mut all_tracked_buffers = Vec::new();
 767
 768            let result = editor.update(cx, |editor, cx| {
 769                let text = editor.text(cx);
 770                let (mut ix, _) = text
 771                    .char_indices()
 772                    .find(|(_, c)| !c.is_whitespace())
 773                    .unwrap_or((0, '\0'));
 774                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 775                editor.display_map.update(cx, |map, cx| {
 776                    let snapshot = map.snapshot(cx);
 777                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 778                        let Some((uri, mention)) = contents.get(&crease_id) else {
 779                            continue;
 780                        };
 781
 782                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
 783                        if crease_range.start.0 > ix {
 784                            let chunk = text[ix..crease_range.start.0].into();
 785                            chunks.push(chunk);
 786                        }
 787                        let chunk = match mention {
 788                            Mention::Text {
 789                                content,
 790                                tracked_buffers,
 791                            } => {
 792                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 793                                if supports_embedded_context {
 794                                    acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 795                                        acp::EmbeddedResourceResource::TextResourceContents(
 796                                            acp::TextResourceContents::new(
 797                                                content.clone(),
 798                                                uri.to_uri().to_string(),
 799                                            ),
 800                                        ),
 801                                    ))
 802                                } else {
 803                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
 804                                        uri.name(),
 805                                        uri.to_uri().to_string(),
 806                                    ))
 807                                }
 808                            }
 809                            Mention::Image(mention_image) => acp::ContentBlock::Image(
 810                                acp::ImageContent::new(
 811                                    mention_image.data.clone(),
 812                                    mention_image.format.mime_type(),
 813                                )
 814                                .uri(match uri {
 815                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 816                                    MentionUri::PastedImage { .. } => {
 817                                        Some(uri.to_uri().to_string())
 818                                    }
 819                                    other => {
 820                                        debug_panic!(
 821                                            "unexpected mention uri for image: {:?}",
 822                                            other
 823                                        );
 824                                        None
 825                                    }
 826                                }),
 827                            ),
 828                            Mention::Link => acp::ContentBlock::ResourceLink(
 829                                acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
 830                            ),
 831                        };
 832                        chunks.push(chunk);
 833                        ix = crease_range.end.0;
 834                    }
 835
 836                    if ix < text.len() {
 837                        let last_chunk = text[ix..].trim_end().to_owned();
 838                        if !last_chunk.is_empty() {
 839                            chunks.push(last_chunk.into());
 840                        }
 841                    }
 842                });
 843                anyhow::Ok((chunks, all_tracked_buffers))
 844            })?;
 845            Ok(result)
 846        })
 847    }
 848
 849    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 850        self.editor.update(cx, |editor, cx| {
 851            editor.clear(window, cx);
 852            editor.remove_creases(
 853                self.mention_set.update(cx, |mention_set, _cx| {
 854                    mention_set
 855                        .clear()
 856                        .map(|(crease_id, _)| crease_id)
 857                        .collect::<Vec<_>>()
 858                }),
 859                cx,
 860            )
 861        });
 862    }
 863
 864    pub fn send(&mut self, cx: &mut Context<Self>) {
 865        if !self.is_empty(cx) {
 866            self.editor.update(cx, |editor, cx| {
 867                editor.clear_inlay_hints(cx);
 868            });
 869        }
 870        cx.emit(MessageEditorEvent::Send)
 871    }
 872
 873    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 874        self.insert_context_prefix("@", window, cx);
 875    }
 876
 877    pub fn insert_context_type(
 878        &mut self,
 879        context_keyword: &str,
 880        window: &mut Window,
 881        cx: &mut Context<Self>,
 882    ) {
 883        let prefix = format!("@{}", context_keyword);
 884        self.insert_context_prefix(&prefix, window, cx);
 885    }
 886
 887    fn insert_context_prefix(&mut self, prefix: &str, window: &mut Window, cx: &mut Context<Self>) {
 888        let editor = self.editor.clone();
 889        let prefix = prefix.to_string();
 890
 891        cx.spawn_in(window, async move |_, cx| {
 892            editor
 893                .update_in(cx, |editor, window, cx| {
 894                    let menu_is_open =
 895                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 896                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 897                        });
 898
 899                    let has_prefix = {
 900                        let snapshot = editor.display_snapshot(cx);
 901                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 902                        let offset = cursor.to_offset(&snapshot);
 903                        let buffer_snapshot = snapshot.buffer_snapshot();
 904                        let prefix_char_count = prefix.chars().count();
 905                        buffer_snapshot
 906                            .reversed_chars_at(offset)
 907                            .take(prefix_char_count)
 908                            .eq(prefix.chars().rev())
 909                    };
 910
 911                    if menu_is_open && has_prefix {
 912                        return;
 913                    }
 914
 915                    editor.insert(&prefix, window, cx);
 916                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 917                })
 918                .log_err();
 919        })
 920        .detach();
 921    }
 922
 923    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 924        self.send(cx);
 925    }
 926
 927    fn send_immediately(&mut self, _: &SendImmediately, _: &mut Window, cx: &mut Context<Self>) {
 928        if self.is_empty(cx) {
 929            return;
 930        }
 931
 932        self.editor.update(cx, |editor, cx| {
 933            editor.clear_inlay_hints(cx);
 934        });
 935
 936        cx.emit(MessageEditorEvent::SendImmediately)
 937    }
 938
 939    fn chat_with_follow(
 940        &mut self,
 941        _: &ChatWithFollow,
 942        window: &mut Window,
 943        cx: &mut Context<Self>,
 944    ) {
 945        self.workspace
 946            .update(cx, |this, cx| {
 947                this.follow(CollaboratorId::Agent, window, cx)
 948            })
 949            .log_err();
 950
 951        self.send(cx);
 952    }
 953
 954    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 955        cx.emit(MessageEditorEvent::Cancel)
 956    }
 957
 958    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 959        let Some(workspace) = self.workspace.upgrade() else {
 960            return;
 961        };
 962        let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
 963            item.entries().iter().find_map(|entry| match entry {
 964                ClipboardEntry::String(text) => {
 965                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
 966                }
 967                _ => None,
 968            })
 969        });
 970
 971        // Insert creases for pasted clipboard selections that:
 972        // 1. Contain exactly one selection
 973        // 2. Have an associated file path
 974        // 3. Span multiple lines (not single-line selections)
 975        // 4. Belong to a file that exists in the current project
 976        let should_insert_creases = util::maybe!({
 977            let selections = editor_clipboard_selections.as_ref()?;
 978            if selections.len() > 1 {
 979                return Some(false);
 980            }
 981            let selection = selections.first()?;
 982            let file_path = selection.file_path.as_ref()?;
 983            let line_range = selection.line_range.as_ref()?;
 984
 985            if line_range.start() == line_range.end() {
 986                return Some(false);
 987            }
 988
 989            Some(
 990                workspace
 991                    .read(cx)
 992                    .project()
 993                    .read(cx)
 994                    .project_path_for_absolute_path(file_path, cx)
 995                    .is_some(),
 996            )
 997        })
 998        .unwrap_or(false);
 999
1000        if should_insert_creases && let Some(selections) = editor_clipboard_selections {
1001            cx.stop_propagation();
1002            let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1003            let (insertion_target, _) = snapshot
1004                .anchor_to_buffer_anchor(self.editor.read(cx).selections.newest_anchor().start)
1005                .unwrap();
1006
1007            let project = workspace.read(cx).project().clone();
1008            for selection in selections {
1009                if let (Some(file_path), Some(line_range)) =
1010                    (selection.file_path, selection.line_range)
1011                {
1012                    let crease_text =
1013                        acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
1014
1015                    let mention_uri = MentionUri::Selection {
1016                        abs_path: Some(file_path.clone()),
1017                        line_range: line_range.clone(),
1018                    };
1019
1020                    let mention_text = mention_uri.as_link().to_string();
1021                    let (text_anchor, content_len) = self.editor.update(cx, |editor, cx| {
1022                        let buffer = editor.buffer().read(cx);
1023                        let snapshot = buffer.snapshot(cx);
1024                        let buffer_snapshot = snapshot.as_singleton().unwrap();
1025                        let text_anchor = insertion_target.bias_left(&buffer_snapshot);
1026
1027                        editor.insert(&mention_text, window, cx);
1028                        editor.insert(" ", window, cx);
1029
1030                        (text_anchor, mention_text.len())
1031                    });
1032
1033                    let Some((crease_id, tx)) = insert_crease_for_mention(
1034                        text_anchor,
1035                        content_len,
1036                        crease_text.into(),
1037                        mention_uri.icon_path(cx),
1038                        mention_uri.tooltip_text(),
1039                        Some(mention_uri.clone()),
1040                        Some(self.workspace.clone()),
1041                        None,
1042                        self.editor.clone(),
1043                        window,
1044                        cx,
1045                    ) else {
1046                        continue;
1047                    };
1048                    drop(tx);
1049
1050                    let mention_task = cx
1051                        .spawn({
1052                            let project = project.clone();
1053                            async move |_, cx| {
1054                                let project_path = project
1055                                    .update(cx, |project, cx| {
1056                                        project.project_path_for_absolute_path(&file_path, cx)
1057                                    })
1058                                    .ok_or_else(|| "project path not found".to_string())?;
1059
1060                                let buffer = project
1061                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
1062                                    .await
1063                                    .map_err(|e| e.to_string())?;
1064
1065                                Ok(buffer.update(cx, |buffer, cx| {
1066                                    let start =
1067                                        Point::new(*line_range.start(), 0).min(buffer.max_point());
1068                                    let end = Point::new(*line_range.end() + 1, 0)
1069                                        .min(buffer.max_point());
1070                                    let content = buffer.text_for_range(start..end).collect();
1071                                    Mention::Text {
1072                                        content,
1073                                        tracked_buffers: vec![cx.entity()],
1074                                    }
1075                                }))
1076                            }
1077                        })
1078                        .shared();
1079
1080                    self.mention_set.update(cx, |mention_set, _cx| {
1081                        mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
1082                    });
1083                }
1084            }
1085            return;
1086        }
1087        // Handle text paste with potential markdown mention links before
1088        // clipboard context entries so markdown text still pastes as text.
1089        if let Some(clipboard_text) = cx.read_from_clipboard().and_then(|item| {
1090            item.entries().iter().find_map(|entry| match entry {
1091                ClipboardEntry::String(text) => Some(text.text().to_string()),
1092                _ => None,
1093            })
1094        }) {
1095            if clipboard_text.contains("[@") {
1096                cx.stop_propagation();
1097                let selections_before = self.editor.update(cx, |editor, cx| {
1098                    let snapshot = editor.buffer().read(cx).snapshot(cx);
1099                    editor
1100                        .selections
1101                        .disjoint_anchors()
1102                        .iter()
1103                        .map(|selection| {
1104                            (
1105                                selection.start.bias_left(&snapshot),
1106                                selection.end.bias_right(&snapshot),
1107                            )
1108                        })
1109                        .collect::<Vec<_>>()
1110                });
1111
1112                self.editor.update(cx, |editor, cx| {
1113                    editor.insert(&clipboard_text, window, cx);
1114                });
1115
1116                let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1117                let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1118
1119                let mut all_mentions = Vec::new();
1120                for (start_anchor, end_anchor) in selections_before {
1121                    let start_offset = start_anchor.to_offset(&snapshot);
1122                    let end_offset = end_anchor.to_offset(&snapshot);
1123
1124                    // Get the actual inserted text from the buffer (may differ due to auto-indent)
1125                    let inserted_text: String =
1126                        snapshot.text_for_range(start_offset..end_offset).collect();
1127
1128                    let parsed_mentions = parse_mention_links(&inserted_text, path_style);
1129                    for (range, mention_uri) in parsed_mentions {
1130                        let mention_start_offset = MultiBufferOffset(start_offset.0 + range.start);
1131                        let anchor = snapshot.anchor_before(mention_start_offset);
1132                        let content_len = range.end - range.start;
1133                        all_mentions.push((anchor, content_len, mention_uri));
1134                    }
1135                }
1136
1137                if !all_mentions.is_empty() {
1138                    let supports_images = self.session_capabilities.read().supports_images();
1139                    let http_client = workspace.read(cx).client().http_client();
1140
1141                    for (anchor, content_len, mention_uri) in all_mentions {
1142                        let Some((crease_id, tx)) = insert_crease_for_mention(
1143                            snapshot.anchor_to_buffer_anchor(anchor).unwrap().0,
1144                            content_len,
1145                            mention_uri.name().into(),
1146                            mention_uri.icon_path(cx),
1147                            mention_uri.tooltip_text(),
1148                            Some(mention_uri.clone()),
1149                            Some(self.workspace.clone()),
1150                            None,
1151                            self.editor.clone(),
1152                            window,
1153                            cx,
1154                        ) else {
1155                            continue;
1156                        };
1157
1158                        // Create the confirmation task based on the mention URI type.
1159                        // This properly loads file content, fetches URLs, etc.
1160                        let task = self.mention_set.update(cx, |mention_set, cx| {
1161                            mention_set.confirm_mention_for_uri(
1162                                mention_uri.clone(),
1163                                supports_images,
1164                                http_client.clone(),
1165                                cx,
1166                            )
1167                        });
1168                        let task = cx
1169                            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
1170                            .shared();
1171
1172                        self.mention_set.update(cx, |mention_set, _cx| {
1173                            mention_set.insert_mention(crease_id, mention_uri.clone(), task.clone())
1174                        });
1175
1176                        // Drop the tx after inserting to signal the crease is ready
1177                        drop(tx);
1178                    }
1179                    return;
1180                }
1181            }
1182        }
1183
1184        if self.handle_pasted_context(window, cx) {
1185            return;
1186        }
1187
1188        // Fall through to default editor paste
1189        cx.propagate();
1190    }
1191
1192    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
1193        let editor = self.editor.clone();
1194        window.defer(cx, move |window, cx| {
1195            editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
1196        });
1197    }
1198
1199    fn handle_pasted_context(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1200        let Some(clipboard) = cx.read_from_clipboard() else {
1201            return false;
1202        };
1203
1204        if matches!(
1205            clipboard.entries().first(),
1206            Some(ClipboardEntry::String(_)) | None
1207        ) {
1208            return false;
1209        }
1210
1211        let Some(workspace) = self.workspace.upgrade() else {
1212            return false;
1213        };
1214        let project = workspace.read(cx).project().clone();
1215        let project_is_local = project.read(cx).is_local();
1216        let supports_images = self.session_capabilities.read().supports_images();
1217        if !project_is_local && !supports_images {
1218            return false;
1219        }
1220        let editor = self.editor.clone();
1221        let mention_set = self.mention_set.clone();
1222        let workspace = self.workspace.clone();
1223        let entries = clipboard.into_entries().collect::<Vec<_>>();
1224
1225        cx.stop_propagation();
1226
1227        window
1228            .spawn(cx, async move |mut cx| {
1229                let (items, added_worktrees) = resolve_pasted_context_items(
1230                    project,
1231                    project_is_local,
1232                    supports_images,
1233                    entries,
1234                    &mut cx,
1235                )
1236                .await;
1237                insert_resolved_pasted_context_items(
1238                    items,
1239                    added_worktrees,
1240                    editor,
1241                    mention_set,
1242                    workspace,
1243                    supports_images,
1244                    &mut cx,
1245                )
1246                .await;
1247                Ok::<(), anyhow::Error>(())
1248            })
1249            .detach_and_log_err(cx);
1250
1251        true
1252    }
1253
1254    pub fn insert_dragged_files(
1255        &mut self,
1256        paths: Vec<project::ProjectPath>,
1257        added_worktrees: Vec<Entity<Worktree>>,
1258        window: &mut Window,
1259        cx: &mut Context<Self>,
1260    ) {
1261        let Some(workspace) = self.workspace.upgrade() else {
1262            return;
1263        };
1264        let project = workspace.read(cx).project().clone();
1265        let supports_images = self.session_capabilities.read().supports_images();
1266        let mut tasks = Vec::new();
1267        for path in paths {
1268            if let Some(task) = insert_mention_for_project_path(
1269                &path,
1270                MentionInsertPosition::EndOfBuffer,
1271                &self.editor,
1272                &self.mention_set,
1273                &project,
1274                &workspace,
1275                supports_images,
1276                window,
1277                cx,
1278            ) {
1279                tasks.push(task);
1280            }
1281        }
1282        cx.spawn(async move |_, _| {
1283            join_all(tasks).await;
1284            drop(added_worktrees);
1285        })
1286        .detach();
1287    }
1288
1289    /// Inserts code snippets as creases into the editor.
1290    /// Each tuple contains (code_text, crease_title).
1291    pub fn insert_code_creases(
1292        &mut self,
1293        creases: Vec<(String, String)>,
1294        window: &mut Window,
1295        cx: &mut Context<Self>,
1296    ) {
1297        self.editor.update(cx, |editor, cx| {
1298            editor.insert("\n", window, cx);
1299        });
1300        for (text, crease_title) in creases {
1301            self.insert_crease_impl(text, crease_title, IconName::TextSnippet, true, window, cx);
1302        }
1303    }
1304
1305    pub fn insert_branch_diff_crease(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1306        let Some(workspace) = self.workspace.upgrade() else {
1307            return;
1308        };
1309
1310        let project = workspace.read(cx).project().clone();
1311
1312        let Some(repo) = project.read(cx).active_repository(cx) else {
1313            return;
1314        };
1315
1316        let default_branch_receiver = repo.update(cx, |repo, _| repo.default_branch(false));
1317        let editor = self.editor.clone();
1318        let mention_set = self.mention_set.clone();
1319        let weak_workspace = self.workspace.clone();
1320
1321        window
1322            .spawn(cx, async move |cx| {
1323                let base_ref: SharedString = default_branch_receiver
1324                    .await
1325                    .ok()
1326                    .and_then(|r| r.ok())
1327                    .flatten()
1328                    .ok_or_else(|| anyhow!("Could not determine default branch"))?;
1329
1330                cx.update(|window, cx| {
1331                    let mention_uri = MentionUri::GitDiff {
1332                        base_ref: base_ref.to_string(),
1333                    };
1334                    let mention_text = mention_uri.as_link().to_string();
1335
1336                    let (text_anchor, content_len) = editor.update(cx, |editor, cx| {
1337                        let buffer = editor.buffer().read(cx);
1338                        let snapshot = buffer.snapshot(cx);
1339                        let buffer_snapshot = snapshot.as_singleton().unwrap();
1340                        let text_anchor = snapshot
1341                            .anchor_to_buffer_anchor(editor.selections.newest_anchor().start)
1342                            .unwrap()
1343                            .0
1344                            .bias_left(&buffer_snapshot);
1345
1346                        editor.insert(&mention_text, window, cx);
1347                        editor.insert(" ", window, cx);
1348
1349                        (text_anchor, mention_text.len())
1350                    });
1351
1352                    let Some((crease_id, tx)) = insert_crease_for_mention(
1353                        text_anchor,
1354                        content_len,
1355                        mention_uri.name().into(),
1356                        mention_uri.icon_path(cx),
1357                        mention_uri.tooltip_text(),
1358                        Some(mention_uri.clone()),
1359                        Some(weak_workspace),
1360                        None,
1361                        editor,
1362                        window,
1363                        cx,
1364                    ) else {
1365                        return;
1366                    };
1367                    drop(tx);
1368
1369                    let confirm_task = mention_set.update(cx, |mention_set, cx| {
1370                        mention_set.confirm_mention_for_git_diff(base_ref, cx)
1371                    });
1372
1373                    let mention_task = cx
1374                        .spawn(async move |_cx| confirm_task.await.map_err(|e| e.to_string()))
1375                        .shared();
1376
1377                    mention_set.update(cx, |mention_set, _| {
1378                        mention_set.insert_mention(crease_id, mention_uri, mention_task);
1379                    });
1380                })
1381            })
1382            .detach_and_log_err(cx);
1383    }
1384
1385    fn insert_crease_impl(
1386        &mut self,
1387        text: String,
1388        title: String,
1389        icon: IconName,
1390        add_trailing_newline: bool,
1391        window: &mut Window,
1392        cx: &mut Context<Self>,
1393    ) {
1394        use editor::display_map::{Crease, FoldPlaceholder};
1395        use multi_buffer::MultiBufferRow;
1396        use rope::Point;
1397
1398        self.editor.update(cx, |editor, cx| {
1399            let point = editor
1400                .selections
1401                .newest::<Point>(&editor.display_snapshot(cx))
1402                .head();
1403            let start_row = MultiBufferRow(point.row);
1404
1405            editor.insert(&text, window, cx);
1406
1407            let snapshot = editor.buffer().read(cx).snapshot(cx);
1408            let anchor_before = snapshot.anchor_after(point);
1409            let anchor_after = editor
1410                .selections
1411                .newest_anchor()
1412                .head()
1413                .bias_left(&snapshot);
1414
1415            if add_trailing_newline {
1416                editor.insert("\n", window, cx);
1417            }
1418
1419            let fold_placeholder = FoldPlaceholder {
1420                render: Arc::new({
1421                    let title = title.clone();
1422                    move |_fold_id, _fold_range, _cx| {
1423                        Button::new("crease", title.clone())
1424                            .layer(ElevationIndex::ElevatedSurface)
1425                            .start_icon(Icon::new(icon))
1426                            .into_any_element()
1427                    }
1428                }),
1429                merge_adjacent: false,
1430                ..Default::default()
1431            };
1432
1433            let crease = Crease::inline(
1434                anchor_before..anchor_after,
1435                fold_placeholder,
1436                |row, is_folded, fold, _window, _cx| {
1437                    Disclosure::new(("crease-toggle", row.0 as u64), !is_folded)
1438                        .toggle_state(is_folded)
1439                        .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
1440                        .into_any_element()
1441                },
1442                |_, _, _, _| gpui::Empty.into_any(),
1443            );
1444            editor.insert_creases(vec![crease], cx);
1445            editor.fold_at(start_row, window, cx);
1446        });
1447    }
1448
1449    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1450        let editor = self.editor.read(cx);
1451        let editor_buffer = editor.buffer().read(cx);
1452        let Some(buffer) = editor_buffer.as_singleton() else {
1453            return;
1454        };
1455        let cursor_anchor = editor.selections.newest_anchor().head();
1456        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1457        let anchor = buffer.update(cx, |buffer, _cx| {
1458            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
1459        });
1460        let Some(workspace) = self.workspace.upgrade() else {
1461            return;
1462        };
1463        let Some(completion) =
1464            PromptCompletionProvider::<MessageEditorCompletionDelegate>::completion_for_action(
1465                PromptContextAction::AddSelections,
1466                anchor..anchor,
1467                self.editor.downgrade(),
1468                self.mention_set.downgrade(),
1469                &workspace,
1470                cx,
1471            )
1472        else {
1473            return;
1474        };
1475
1476        self.editor.update(cx, |message_editor, cx| {
1477            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1478            message_editor.request_autoscroll(Autoscroll::fit(), cx);
1479        });
1480        if let Some(confirm) = completion.confirm {
1481            confirm(CompletionIntent::Complete, window, cx);
1482        }
1483    }
1484
1485    pub fn add_images_from_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1486        if !self.session_capabilities.read().supports_images() {
1487            return;
1488        }
1489
1490        let editor = self.editor.clone();
1491        let mention_set = self.mention_set.clone();
1492        let workspace = self.workspace.clone();
1493
1494        let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
1495            files: true,
1496            directories: false,
1497            multiple: true,
1498            prompt: Some("Select Images".into()),
1499        });
1500
1501        window
1502            .spawn(cx, async move |cx| {
1503                let paths = match paths_receiver.await {
1504                    Ok(Ok(Some(paths))) => paths,
1505                    _ => return Ok::<(), anyhow::Error>(()),
1506                };
1507
1508                let default_image_name: SharedString = "Image".into();
1509                let images = cx
1510                    .background_spawn(async move {
1511                        paths
1512                            .into_iter()
1513                            .filter_map(|path| {
1514                                crate::mention_set::load_external_image_from_path(
1515                                    &path,
1516                                    &default_image_name,
1517                                )
1518                            })
1519                            .collect::<Vec<_>>()
1520                    })
1521                    .await;
1522
1523                crate::mention_set::insert_images_as_context(
1524                    images,
1525                    editor,
1526                    mention_set,
1527                    workspace,
1528                    cx,
1529                )
1530                .await;
1531                Ok(())
1532            })
1533            .detach_and_log_err(cx);
1534    }
1535
1536    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1537        self.editor.update(cx, |message_editor, cx| {
1538            message_editor.set_read_only(read_only);
1539            cx.notify()
1540        })
1541    }
1542
1543    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1544        self.editor.update(cx, |editor, cx| {
1545            if *editor.mode() != mode {
1546                editor.set_mode(mode);
1547                cx.notify()
1548            }
1549        });
1550    }
1551
1552    pub fn set_message(
1553        &mut self,
1554        message: Vec<acp::ContentBlock>,
1555        window: &mut Window,
1556        cx: &mut Context<Self>,
1557    ) {
1558        self.clear(window, cx);
1559        self.insert_message_blocks(message, false, window, cx);
1560    }
1561
1562    pub fn append_message(
1563        &mut self,
1564        message: Vec<acp::ContentBlock>,
1565        separator: Option<&str>,
1566        window: &mut Window,
1567        cx: &mut Context<Self>,
1568    ) {
1569        if message.is_empty() {
1570            return;
1571        }
1572
1573        if let Some(separator) = separator
1574            && !separator.is_empty()
1575            && !self.is_empty(cx)
1576        {
1577            self.editor.update(cx, |editor, cx| {
1578                editor.insert(separator, window, cx);
1579            });
1580        }
1581
1582        self.insert_message_blocks(message, true, window, cx);
1583    }
1584
1585    fn insert_message_blocks(
1586        &mut self,
1587        message: Vec<acp::ContentBlock>,
1588        append_to_existing: bool,
1589        window: &mut Window,
1590        cx: &mut Context<Self>,
1591    ) {
1592        let Some(workspace) = self.workspace.upgrade() else {
1593            return;
1594        };
1595
1596        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1597        let mut text = String::new();
1598        let mut mentions = Vec::new();
1599
1600        for chunk in message {
1601            match chunk {
1602                acp::ContentBlock::Text(text_content) => {
1603                    text.push_str(&text_content.text);
1604                }
1605                acp::ContentBlock::Resource(acp::EmbeddedResource {
1606                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1607                    ..
1608                }) => {
1609                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1610                    else {
1611                        continue;
1612                    };
1613                    let start = text.len();
1614                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1615                    let end = text.len();
1616                    mentions.push((
1617                        start..end,
1618                        mention_uri,
1619                        Mention::Text {
1620                            content: resource.text,
1621                            tracked_buffers: Vec::new(),
1622                        },
1623                    ));
1624                }
1625                acp::ContentBlock::ResourceLink(resource) => {
1626                    if let Some(mention_uri) =
1627                        MentionUri::parse(&resource.uri, path_style).log_err()
1628                    {
1629                        let start = text.len();
1630                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1631                        let end = text.len();
1632                        mentions.push((start..end, mention_uri, Mention::Link));
1633                    }
1634                }
1635                acp::ContentBlock::Image(acp::ImageContent {
1636                    uri,
1637                    data,
1638                    mime_type,
1639                    ..
1640                }) => {
1641                    let mention_uri = if let Some(uri) = uri {
1642                        MentionUri::parse(&uri, path_style)
1643                    } else {
1644                        Ok(MentionUri::PastedImage {
1645                            name: "Image".to_string(),
1646                        })
1647                    };
1648                    let Some(mention_uri) = mention_uri.log_err() else {
1649                        continue;
1650                    };
1651                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1652                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1653                        continue;
1654                    };
1655                    let start = text.len();
1656                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1657                    let end = text.len();
1658                    mentions.push((
1659                        start..end,
1660                        mention_uri,
1661                        Mention::Image(MentionImage {
1662                            data: data.into(),
1663                            format,
1664                        }),
1665                    ));
1666                }
1667                _ => {}
1668            }
1669        }
1670
1671        if text.is_empty() && mentions.is_empty() {
1672            return;
1673        }
1674
1675        let insertion_start = if append_to_existing {
1676            self.editor.read(cx).text(cx).len()
1677        } else {
1678            0
1679        };
1680
1681        let snapshot = if append_to_existing {
1682            self.editor.update(cx, |editor, cx| {
1683                editor.insert(&text, window, cx);
1684                editor.buffer().read(cx).snapshot(cx)
1685            })
1686        } else {
1687            self.editor.update(cx, |editor, cx| {
1688                editor.set_text(text, window, cx);
1689                editor.buffer().read(cx).snapshot(cx)
1690            })
1691        };
1692
1693        for (range, mention_uri, mention) in mentions {
1694            let adjusted_start = insertion_start + range.start;
1695            let anchor = snapshot.anchor_before(MultiBufferOffset(adjusted_start));
1696            let Some((crease_id, tx)) = insert_crease_for_mention(
1697                snapshot.anchor_to_buffer_anchor(anchor).unwrap().0,
1698                range.end - range.start,
1699                mention_uri.name().into(),
1700                mention_uri.icon_path(cx),
1701                mention_uri.tooltip_text(),
1702                Some(mention_uri.clone()),
1703                Some(self.workspace.clone()),
1704                None,
1705                self.editor.clone(),
1706                window,
1707                cx,
1708            ) else {
1709                continue;
1710            };
1711            drop(tx);
1712
1713            self.mention_set.update(cx, |mention_set, _cx| {
1714                mention_set.insert_mention(
1715                    crease_id,
1716                    mention_uri.clone(),
1717                    Task::ready(Ok(mention)).shared(),
1718                )
1719            });
1720        }
1721
1722        cx.notify();
1723    }
1724
1725    pub fn text(&self, cx: &App) -> String {
1726        self.editor.read(cx).text(cx)
1727    }
1728
1729    pub fn set_cursor_offset(
1730        &mut self,
1731        offset: usize,
1732        window: &mut Window,
1733        cx: &mut Context<Self>,
1734    ) {
1735        self.editor.update(cx, |editor, cx| {
1736            let snapshot = editor.buffer().read(cx).snapshot(cx);
1737            let offset = snapshot.clip_offset(MultiBufferOffset(offset), text::Bias::Left);
1738            editor.change_selections(Default::default(), window, cx, |selections| {
1739                selections.select_ranges([offset..offset]);
1740            });
1741        });
1742    }
1743
1744    pub fn insert_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1745        if text.is_empty() {
1746            return;
1747        }
1748
1749        self.editor.update(cx, |editor, cx| {
1750            editor.insert(text, window, cx);
1751        });
1752    }
1753
1754    pub fn set_placeholder_text(
1755        &mut self,
1756        placeholder: &str,
1757        window: &mut Window,
1758        cx: &mut Context<Self>,
1759    ) {
1760        self.editor.update(cx, |editor, cx| {
1761            editor.set_placeholder_text(placeholder, window, cx);
1762        });
1763    }
1764
1765    #[cfg(any(test, feature = "test-support"))]
1766    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1767        self.editor.update(cx, |editor, cx| {
1768            editor.set_text(text, window, cx);
1769        });
1770    }
1771}
1772
1773impl Focusable for MessageEditor {
1774    fn focus_handle(&self, cx: &App) -> FocusHandle {
1775        self.editor.focus_handle(cx)
1776    }
1777}
1778
1779impl Render for MessageEditor {
1780    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1781        div()
1782            .key_context("MessageEditor")
1783            .on_action(cx.listener(Self::chat))
1784            .on_action(cx.listener(Self::send_immediately))
1785            .on_action(cx.listener(Self::chat_with_follow))
1786            .on_action(cx.listener(Self::cancel))
1787            .on_action(cx.listener(Self::paste_raw))
1788            .capture_action(cx.listener(Self::paste))
1789            .flex_1()
1790            .child({
1791                let settings = ThemeSettings::get_global(cx);
1792
1793                let text_style = TextStyle {
1794                    color: cx.theme().colors().text,
1795                    font_family: settings.buffer_font.family.clone(),
1796                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1797                    font_features: settings.buffer_font.features.clone(),
1798                    font_size: settings.agent_buffer_font_size(cx).into(),
1799                    font_weight: settings.buffer_font.weight,
1800                    line_height: relative(settings.buffer_line_height.value()),
1801                    ..Default::default()
1802                };
1803
1804                EditorElement::new(
1805                    &self.editor,
1806                    EditorStyle {
1807                        background: cx.theme().colors().editor_background,
1808                        local_player: cx.theme().players().local(),
1809                        text: text_style,
1810                        syntax: cx.theme().syntax().clone(),
1811                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1812                        ..Default::default()
1813                    },
1814                )
1815            })
1816    }
1817}
1818
1819pub struct MessageEditorAddon {}
1820
1821impl MessageEditorAddon {
1822    pub fn new() -> Self {
1823        Self {}
1824    }
1825}
1826
1827impl Addon for MessageEditorAddon {
1828    fn to_any(&self) -> &dyn std::any::Any {
1829        self
1830    }
1831
1832    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1833        Some(self)
1834    }
1835
1836    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1837        let settings = agent_settings::AgentSettings::get_global(cx);
1838        if settings.use_modifier_to_send {
1839            key_context.add("use_modifier_to_send");
1840        }
1841    }
1842}
1843
1844/// Parses markdown mention links in the format `[@name](uri)` from text.
1845/// Returns a vector of (range, MentionUri) pairs where range is the byte range in the text.
1846fn parse_mention_links(text: &str, path_style: PathStyle) -> Vec<(Range<usize>, MentionUri)> {
1847    let mut mentions = Vec::new();
1848    let mut search_start = 0;
1849
1850    while let Some(link_start) = text[search_start..].find("[@") {
1851        let absolute_start = search_start + link_start;
1852
1853        // Find the matching closing bracket for the name, handling nested brackets.
1854        // Start at the '[' character so find_matching_bracket can track depth correctly.
1855        let Some(name_end) = find_matching_bracket(&text[absolute_start..], '[', ']') else {
1856            search_start = absolute_start + 2;
1857            continue;
1858        };
1859        let name_end = absolute_start + name_end;
1860
1861        // Check for opening parenthesis immediately after
1862        if text.get(name_end + 1..name_end + 2) != Some("(") {
1863            search_start = name_end + 1;
1864            continue;
1865        }
1866
1867        // Find the matching closing parenthesis for the URI, handling nested parens
1868        let uri_start = name_end + 2;
1869        let Some(uri_end_relative) = find_matching_bracket(&text[name_end + 1..], '(', ')') else {
1870            search_start = uri_start;
1871            continue;
1872        };
1873        let uri_end = name_end + 1 + uri_end_relative;
1874        let link_end = uri_end + 1;
1875
1876        let uri_str = &text[uri_start..uri_end];
1877
1878        // Try to parse the URI as a MentionUri
1879        if let Ok(mention_uri) = MentionUri::parse(uri_str, path_style) {
1880            mentions.push((absolute_start..link_end, mention_uri));
1881        }
1882
1883        search_start = link_end;
1884    }
1885
1886    mentions
1887}
1888
1889/// Finds the position of the matching closing bracket, handling nested brackets.
1890/// The input `text` should start with the opening bracket.
1891/// Returns the index of the matching closing bracket relative to `text`.
1892fn find_matching_bracket(text: &str, open: char, close: char) -> Option<usize> {
1893    let mut depth = 0;
1894    for (index, character) in text.char_indices() {
1895        if character == open {
1896            depth += 1;
1897        } else if character == close {
1898            depth -= 1;
1899            if depth == 0 {
1900                return Some(index);
1901            }
1902        }
1903    }
1904    None
1905}
1906
1907#[cfg(test)]
1908mod tests {
1909    use std::{ops::Range, path::Path, path::PathBuf, sync::Arc};
1910
1911    use acp_thread::MentionUri;
1912    use agent::{ThreadStore, outline};
1913    use agent_client_protocol as acp;
1914    use base64::Engine as _;
1915    use editor::{
1916        AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset, SelectionEffects,
1917        actions::Paste,
1918    };
1919
1920    use fs::FakeFs;
1921    use futures::StreamExt as _;
1922    use gpui::{
1923        AppContext, ClipboardEntry, ClipboardItem, Entity, EventEmitter, ExternalPaths,
1924        FocusHandle, Focusable, TestAppContext, VisualTestContext,
1925    };
1926    use language_model::LanguageModelRegistry;
1927    use lsp::{CompletionContext, CompletionTriggerKind};
1928    use parking_lot::RwLock;
1929    use project::{CompletionIntent, Project, ProjectPath};
1930    use serde_json::{Value, json};
1931
1932    use text::Point;
1933    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1934    use util::{path, paths::PathStyle, rel_path::rel_path};
1935    use workspace::{AppState, Item, MultiWorkspace};
1936
1937    use crate::completion_provider::PromptContextType;
1938    use crate::{
1939        conversation_view::tests::init_test,
1940        message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
1941    };
1942
1943    #[test]
1944    fn test_parse_mention_links() {
1945        // Single file mention
1946        let text = "[@bundle-mac](file:///Users/test/zed/script/bundle-mac)";
1947        let mentions = parse_mention_links(text, PathStyle::local());
1948        assert_eq!(mentions.len(), 1);
1949        assert_eq!(mentions[0].0, 0..text.len());
1950        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
1951
1952        // Multiple mentions
1953        let text = "Check [@file1](file:///path/to/file1) and [@file2](file:///path/to/file2)!";
1954        let mentions = parse_mention_links(text, PathStyle::local());
1955        assert_eq!(mentions.len(), 2);
1956
1957        // Text without mentions
1958        let text = "Just some regular text without mentions";
1959        let mentions = parse_mention_links(text, PathStyle::local());
1960        assert_eq!(mentions.len(), 0);
1961
1962        // Malformed mentions (should be skipped)
1963        let text = "[@incomplete](invalid://uri) and [@missing](";
1964        let mentions = parse_mention_links(text, PathStyle::local());
1965        assert_eq!(mentions.len(), 0);
1966
1967        // Mixed content with valid mention
1968        let text = "Before [@valid](file:///path/to/file) after";
1969        let mentions = parse_mention_links(text, PathStyle::local());
1970        assert_eq!(mentions.len(), 1);
1971        assert_eq!(mentions[0].0.start, 7);
1972
1973        // HTTP URL mention (Fetch)
1974        let text = "Check out [@docs](https://example.com/docs) for more info";
1975        let mentions = parse_mention_links(text, PathStyle::local());
1976        assert_eq!(mentions.len(), 1);
1977        assert!(matches!(mentions[0].1, MentionUri::Fetch { .. }));
1978
1979        // Directory mention (trailing slash)
1980        let text = "[@src](file:///path/to/src/)";
1981        let mentions = parse_mention_links(text, PathStyle::local());
1982        assert_eq!(mentions.len(), 1);
1983        assert!(matches!(mentions[0].1, MentionUri::Directory { .. }));
1984
1985        // Multiple different mention types
1986        let text = "File [@f](file:///a) and URL [@u](https://b.com) and dir [@d](file:///c/)";
1987        let mentions = parse_mention_links(text, PathStyle::local());
1988        assert_eq!(mentions.len(), 3);
1989        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
1990        assert!(matches!(mentions[1].1, MentionUri::Fetch { .. }));
1991        assert!(matches!(mentions[2].1, MentionUri::Directory { .. }));
1992
1993        // Adjacent mentions without separator
1994        let text = "[@a](file:///a)[@b](file:///b)";
1995        let mentions = parse_mention_links(text, PathStyle::local());
1996        assert_eq!(mentions.len(), 2);
1997
1998        // Regular markdown link (not a mention) should be ignored
1999        let text = "[regular link](https://example.com)";
2000        let mentions = parse_mention_links(text, PathStyle::local());
2001        assert_eq!(mentions.len(), 0);
2002
2003        // Incomplete mention link patterns
2004        let text = "[@name] without url and [@name( malformed";
2005        let mentions = parse_mention_links(text, PathStyle::local());
2006        assert_eq!(mentions.len(), 0);
2007
2008        // Nested brackets in name portion
2009        let text = "[@name [with brackets]](file:///path/to/file)";
2010        let mentions = parse_mention_links(text, PathStyle::local());
2011        assert_eq!(mentions.len(), 1);
2012        assert_eq!(mentions[0].0, 0..text.len());
2013
2014        // Deeply nested brackets
2015        let text = "[@outer [inner [deep]]](file:///path)";
2016        let mentions = parse_mention_links(text, PathStyle::local());
2017        assert_eq!(mentions.len(), 1);
2018
2019        // Unbalanced brackets should fail gracefully
2020        let text = "[@unbalanced [bracket](file:///path)";
2021        let mentions = parse_mention_links(text, PathStyle::local());
2022        assert_eq!(mentions.len(), 0);
2023
2024        // Nested parentheses in URI (common in URLs with query params)
2025        let text = "[@wiki](https://en.wikipedia.org/wiki/Rust_(programming_language))";
2026        let mentions = parse_mention_links(text, PathStyle::local());
2027        assert_eq!(mentions.len(), 1);
2028        if let MentionUri::Fetch { url } = &mentions[0].1 {
2029            assert!(url.as_str().contains("Rust_(programming_language)"));
2030        } else {
2031            panic!("Expected Fetch URI");
2032        }
2033    }
2034
2035    #[gpui::test]
2036    async fn test_at_mention_removal(cx: &mut TestAppContext) {
2037        init_test(cx);
2038
2039        let fs = FakeFs::new(cx.executor());
2040        fs.insert_tree("/project", json!({"file": ""})).await;
2041        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2042
2043        let (multi_workspace, cx) =
2044            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2045        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2046
2047        let thread_store = None;
2048
2049        let message_editor = cx.update(|window, cx| {
2050            cx.new(|cx| {
2051                MessageEditor::new(
2052                    workspace.downgrade(),
2053                    project.downgrade(),
2054                    thread_store.clone(),
2055                    None,
2056                    None,
2057                    Default::default(),
2058                    "Test Agent".into(),
2059                    "Test",
2060                    EditorMode::AutoHeight {
2061                        min_lines: 1,
2062                        max_lines: None,
2063                    },
2064                    window,
2065                    cx,
2066                )
2067            })
2068        });
2069        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2070
2071        cx.run_until_parked();
2072
2073        let completions = editor.update_in(cx, |editor, window, cx| {
2074            editor.set_text("Hello @file ", window, cx);
2075            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
2076            let completion_provider = editor.completion_provider().unwrap();
2077            completion_provider.completions(
2078                &buffer,
2079                text::Anchor::max_for_buffer(buffer.read(cx).remote_id()),
2080                CompletionContext {
2081                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
2082                    trigger_character: Some("@".into()),
2083                },
2084                window,
2085                cx,
2086            )
2087        });
2088        let [_, completion]: [_; 2] = completions
2089            .await
2090            .unwrap()
2091            .into_iter()
2092            .flat_map(|response| response.completions)
2093            .collect::<Vec<_>>()
2094            .try_into()
2095            .unwrap();
2096
2097        editor.update_in(cx, |editor, window, cx| {
2098            let snapshot = editor.buffer().read(cx).snapshot(cx);
2099            let range = snapshot
2100                .buffer_anchor_range_to_anchor_range(completion.replace_range)
2101                .unwrap();
2102            editor.edit([(range, completion.new_text)], cx);
2103            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
2104        });
2105
2106        cx.run_until_parked();
2107
2108        // Backspace over the inserted crease (and the following space).
2109        editor.update_in(cx, |editor, window, cx| {
2110            editor.backspace(&Default::default(), window, cx);
2111            editor.backspace(&Default::default(), window, cx);
2112        });
2113
2114        let (content, _) = message_editor
2115            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2116            .await
2117            .unwrap();
2118
2119        // We don't send a resource link for the deleted crease.
2120        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
2121    }
2122
2123    #[gpui::test]
2124    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
2125        init_test(cx);
2126        let fs = FakeFs::new(cx.executor());
2127        fs.insert_tree(
2128            "/test",
2129            json!({
2130                ".zed": {
2131                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
2132                },
2133                "src": {
2134                    "main.rs": "fn main() {}",
2135                },
2136            }),
2137        )
2138        .await;
2139
2140        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
2141        let thread_store = None;
2142        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2143            acp::PromptCapabilities::default(),
2144            vec![],
2145        )));
2146
2147        let (multi_workspace, cx) =
2148            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2149        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2150        let workspace_handle = workspace.downgrade();
2151        let message_editor = workspace.update_in(cx, |_, window, cx| {
2152            cx.new(|cx| {
2153                MessageEditor::new(
2154                    workspace_handle.clone(),
2155                    project.downgrade(),
2156                    thread_store.clone(),
2157                    None,
2158                    None,
2159                    session_capabilities.clone(),
2160                    "Claude Agent".into(),
2161                    "Test",
2162                    EditorMode::AutoHeight {
2163                        min_lines: 1,
2164                        max_lines: None,
2165                    },
2166                    window,
2167                    cx,
2168                )
2169            })
2170        });
2171        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2172
2173        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
2174        editor.update_in(cx, |editor, window, cx| {
2175            editor.set_text("/file test.txt", window, cx);
2176        });
2177
2178        let contents_result = message_editor
2179            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2180            .await;
2181
2182        // Should fail because available_commands is empty (no commands supported)
2183        assert!(contents_result.is_err());
2184        let error_message = contents_result.unwrap_err().to_string();
2185        assert!(error_message.contains("not supported by Claude Agent"));
2186        assert!(error_message.contains("Available commands: none"));
2187
2188        // Now simulate Claude providing its list of available commands (which doesn't include file)
2189        session_capabilities
2190            .write()
2191            .set_available_commands(vec![acp::AvailableCommand::new("help", "Get help")]);
2192
2193        // Test that unsupported slash commands trigger an error when we have a list of available commands
2194        editor.update_in(cx, |editor, window, cx| {
2195            editor.set_text("/file test.txt", window, cx);
2196        });
2197
2198        let contents_result = message_editor
2199            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2200            .await;
2201
2202        assert!(contents_result.is_err());
2203        let error_message = contents_result.unwrap_err().to_string();
2204        assert!(error_message.contains("not supported by Claude Agent"));
2205        assert!(error_message.contains("/file"));
2206        assert!(error_message.contains("Available commands: /help"));
2207
2208        // Test that supported commands work fine
2209        editor.update_in(cx, |editor, window, cx| {
2210            editor.set_text("/help", window, cx);
2211        });
2212
2213        let contents_result = message_editor
2214            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2215            .await;
2216
2217        // Should succeed because /help is in available_commands
2218        assert!(contents_result.is_ok());
2219
2220        // Test that regular text works fine
2221        editor.update_in(cx, |editor, window, cx| {
2222            editor.set_text("Hello Claude!", window, cx);
2223        });
2224
2225        let (content, _) = message_editor
2226            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2227            .await
2228            .unwrap();
2229
2230        assert_eq!(content.len(), 1);
2231        if let acp::ContentBlock::Text(text) = &content[0] {
2232            assert_eq!(text.text, "Hello Claude!");
2233        } else {
2234            panic!("Expected ContentBlock::Text");
2235        }
2236
2237        // Test that @ mentions still work
2238        editor.update_in(cx, |editor, window, cx| {
2239            editor.set_text("Check this @", window, cx);
2240        });
2241
2242        // The @ mention functionality should not be affected
2243        let (content, _) = message_editor
2244            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2245            .await
2246            .unwrap();
2247
2248        assert_eq!(content.len(), 1);
2249        if let acp::ContentBlock::Text(text) = &content[0] {
2250            assert_eq!(text.text, "Check this @");
2251        } else {
2252            panic!("Expected ContentBlock::Text");
2253        }
2254    }
2255
2256    struct MessageEditorItem(Entity<MessageEditor>);
2257
2258    impl Item for MessageEditorItem {
2259        type Event = ();
2260
2261        fn include_in_nav_history() -> bool {
2262            false
2263        }
2264
2265        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
2266            "Test".into()
2267        }
2268    }
2269
2270    impl EventEmitter<()> for MessageEditorItem {}
2271
2272    impl Focusable for MessageEditorItem {
2273        fn focus_handle(&self, cx: &App) -> FocusHandle {
2274            self.0.read(cx).focus_handle(cx)
2275        }
2276    }
2277
2278    impl Render for MessageEditorItem {
2279        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2280            self.0.clone().into_any_element()
2281        }
2282    }
2283
2284    #[gpui::test]
2285    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
2286        init_test(cx);
2287
2288        let app_state = cx.update(AppState::test);
2289
2290        cx.update(|cx| {
2291            editor::init(cx);
2292            workspace::init(app_state.clone(), cx);
2293        });
2294
2295        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2296        let window =
2297            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2298        let workspace = window
2299            .read_with(cx, |mw, _| mw.workspace().clone())
2300            .unwrap();
2301
2302        let mut cx = VisualTestContext::from_window(window.into(), cx);
2303
2304        let thread_store = None;
2305        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2306            acp::PromptCapabilities::default(),
2307            vec![
2308                acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
2309                acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
2310                    acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
2311                        "<name>",
2312                    )),
2313                ),
2314            ],
2315        )));
2316
2317        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2318            let workspace_handle = cx.weak_entity();
2319            let message_editor = cx.new(|cx| {
2320                MessageEditor::new(
2321                    workspace_handle,
2322                    project.downgrade(),
2323                    thread_store.clone(),
2324                    None,
2325                    None,
2326                    session_capabilities.clone(),
2327                    "Test Agent".into(),
2328                    "Test",
2329                    EditorMode::AutoHeight {
2330                        max_lines: None,
2331                        min_lines: 1,
2332                    },
2333                    window,
2334                    cx,
2335                )
2336            });
2337            workspace.active_pane().update(cx, |pane, cx| {
2338                pane.add_item(
2339                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2340                    true,
2341                    true,
2342                    None,
2343                    window,
2344                    cx,
2345                );
2346            });
2347            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2348            message_editor.read(cx).editor().clone()
2349        });
2350
2351        cx.simulate_input("/");
2352
2353        editor.update_in(&mut cx, |editor, window, cx| {
2354            assert_eq!(editor.text(cx), "/");
2355            assert!(editor.has_visible_completions_menu());
2356
2357            assert_eq!(
2358                current_completion_labels_with_documentation(editor),
2359                &[
2360                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
2361                    ("say-hello".into(), "Say hello to whoever you want".into())
2362                ]
2363            );
2364            editor.set_text("", window, cx);
2365        });
2366
2367        cx.simulate_input("/qui");
2368
2369        editor.update_in(&mut cx, |editor, window, cx| {
2370            assert_eq!(editor.text(cx), "/qui");
2371            assert!(editor.has_visible_completions_menu());
2372
2373            assert_eq!(
2374                current_completion_labels_with_documentation(editor),
2375                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
2376            );
2377            editor.set_text("", window, cx);
2378        });
2379
2380        editor.update_in(&mut cx, |editor, window, cx| {
2381            assert!(editor.has_visible_completions_menu());
2382            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2383        });
2384
2385        cx.run_until_parked();
2386
2387        editor.update_in(&mut cx, |editor, window, cx| {
2388            assert_eq!(editor.display_text(cx), "/quick-math ");
2389            assert!(!editor.has_visible_completions_menu());
2390            editor.set_text("", window, cx);
2391        });
2392
2393        cx.simulate_input("/say");
2394
2395        editor.update_in(&mut cx, |editor, _window, cx| {
2396            assert_eq!(editor.display_text(cx), "/say");
2397            assert!(editor.has_visible_completions_menu());
2398
2399            assert_eq!(
2400                current_completion_labels_with_documentation(editor),
2401                &[("say-hello".into(), "Say hello to whoever you want".into())]
2402            );
2403        });
2404
2405        editor.update_in(&mut cx, |editor, window, cx| {
2406            assert!(editor.has_visible_completions_menu());
2407            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2408        });
2409
2410        cx.run_until_parked();
2411
2412        editor.update_in(&mut cx, |editor, _window, cx| {
2413            assert_eq!(editor.text(cx), "/say-hello ");
2414            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2415            assert!(!editor.has_visible_completions_menu());
2416        });
2417
2418        cx.simulate_input("GPT5");
2419
2420        cx.run_until_parked();
2421
2422        editor.update_in(&mut cx, |editor, window, cx| {
2423            assert_eq!(editor.text(cx), "/say-hello GPT5");
2424            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2425            assert!(!editor.has_visible_completions_menu());
2426
2427            // Delete argument
2428            for _ in 0..5 {
2429                editor.backspace(&editor::actions::Backspace, window, cx);
2430            }
2431        });
2432
2433        cx.run_until_parked();
2434
2435        editor.update_in(&mut cx, |editor, window, cx| {
2436            assert_eq!(editor.text(cx), "/say-hello");
2437            // Hint is visible because argument was deleted
2438            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2439
2440            // Delete last command letter
2441            editor.backspace(&editor::actions::Backspace, window, cx);
2442        });
2443
2444        cx.run_until_parked();
2445
2446        editor.update_in(&mut cx, |editor, _window, cx| {
2447            // Hint goes away once command no longer matches an available one
2448            assert_eq!(editor.text(cx), "/say-hell");
2449            assert_eq!(editor.display_text(cx), "/say-hell");
2450            assert!(!editor.has_visible_completions_menu());
2451        });
2452    }
2453
2454    #[gpui::test]
2455    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2456        init_test(cx);
2457
2458        let app_state = cx.update(AppState::test);
2459
2460        cx.update(|cx| {
2461            editor::init(cx);
2462            workspace::init(app_state.clone(), cx);
2463        });
2464
2465        app_state
2466            .fs
2467            .as_fake()
2468            .insert_tree(
2469                path!("/dir"),
2470                json!({
2471                    "editor": "",
2472                    "a": {
2473                        "one.txt": "1",
2474                        "two.txt": "2",
2475                        "three.txt": "3",
2476                        "four.txt": "4"
2477                    },
2478                    "b": {
2479                        "five.txt": "5",
2480                        "six.txt": "6",
2481                        "seven.txt": "7",
2482                        "eight.txt": "8",
2483                    },
2484                    "x.png": "",
2485                }),
2486            )
2487            .await;
2488
2489        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2490        let window =
2491            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2492        let workspace = window
2493            .read_with(cx, |mw, _| mw.workspace().clone())
2494            .unwrap();
2495
2496        let worktree = project.update(cx, |project, cx| {
2497            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2498            assert_eq!(worktrees.len(), 1);
2499            worktrees.pop().unwrap()
2500        });
2501        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2502
2503        let mut cx = VisualTestContext::from_window(window.into(), cx);
2504
2505        let paths = vec![
2506            rel_path("a/one.txt"),
2507            rel_path("a/two.txt"),
2508            rel_path("a/three.txt"),
2509            rel_path("a/four.txt"),
2510            rel_path("b/five.txt"),
2511            rel_path("b/six.txt"),
2512            rel_path("b/seven.txt"),
2513            rel_path("b/eight.txt"),
2514        ];
2515
2516        let slash = PathStyle::local().primary_separator();
2517
2518        let mut opened_editors = Vec::new();
2519        for path in paths {
2520            let buffer = workspace
2521                .update_in(&mut cx, |workspace, window, cx| {
2522                    workspace.open_path(
2523                        ProjectPath {
2524                            worktree_id,
2525                            path: path.into(),
2526                        },
2527                        None,
2528                        false,
2529                        window,
2530                        cx,
2531                    )
2532                })
2533                .await
2534                .unwrap();
2535            opened_editors.push(buffer);
2536        }
2537
2538        let thread_store = cx.new(|cx| ThreadStore::new(cx));
2539        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2540            acp::PromptCapabilities::default(),
2541            vec![],
2542        )));
2543
2544        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2545            let workspace_handle = cx.weak_entity();
2546            let message_editor = cx.new(|cx| {
2547                MessageEditor::new(
2548                    workspace_handle,
2549                    project.downgrade(),
2550                    Some(thread_store),
2551                    None,
2552                    None,
2553                    session_capabilities.clone(),
2554                    "Test Agent".into(),
2555                    "Test",
2556                    EditorMode::AutoHeight {
2557                        max_lines: None,
2558                        min_lines: 1,
2559                    },
2560                    window,
2561                    cx,
2562                )
2563            });
2564            workspace.active_pane().update(cx, |pane, cx| {
2565                pane.add_item(
2566                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2567                    true,
2568                    true,
2569                    None,
2570                    window,
2571                    cx,
2572                );
2573            });
2574            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2575            let editor = message_editor.read(cx).editor().clone();
2576            (message_editor, editor)
2577        });
2578
2579        cx.simulate_input("Lorem @");
2580
2581        editor.update_in(&mut cx, |editor, window, cx| {
2582            assert_eq!(editor.text(cx), "Lorem @");
2583            assert!(editor.has_visible_completions_menu());
2584
2585            assert_eq!(
2586                current_completion_labels(editor),
2587                &[
2588                    format!("eight.txt b{slash}"),
2589                    format!("seven.txt b{slash}"),
2590                    format!("six.txt b{slash}"),
2591                    format!("five.txt b{slash}"),
2592                    "Files & Directories".into(),
2593                    "Symbols".into()
2594                ]
2595            );
2596            editor.set_text("", window, cx);
2597        });
2598
2599        message_editor.update(&mut cx, |editor, _cx| {
2600            editor.session_capabilities.write().set_prompt_capabilities(
2601                acp::PromptCapabilities::new()
2602                    .image(true)
2603                    .audio(true)
2604                    .embedded_context(true),
2605            );
2606        });
2607
2608        cx.simulate_input("Lorem ");
2609
2610        editor.update(&mut cx, |editor, cx| {
2611            assert_eq!(editor.text(cx), "Lorem ");
2612            assert!(!editor.has_visible_completions_menu());
2613        });
2614
2615        cx.simulate_input("@");
2616
2617        editor.update(&mut cx, |editor, cx| {
2618            assert_eq!(editor.text(cx), "Lorem @");
2619            assert!(editor.has_visible_completions_menu());
2620            assert_eq!(
2621                current_completion_labels(editor),
2622                &[
2623                    format!("eight.txt b{slash}"),
2624                    format!("seven.txt b{slash}"),
2625                    format!("six.txt b{slash}"),
2626                    format!("five.txt b{slash}"),
2627                    "Files & Directories".into(),
2628                    "Symbols".into(),
2629                    "Threads".into(),
2630                    "Fetch".into()
2631                ]
2632            );
2633        });
2634
2635        // Select and confirm "File"
2636        editor.update_in(&mut cx, |editor, window, cx| {
2637            assert!(editor.has_visible_completions_menu());
2638            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2639            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2640            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2641            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2642            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2643        });
2644
2645        cx.run_until_parked();
2646
2647        editor.update(&mut cx, |editor, cx| {
2648            assert_eq!(editor.text(cx), "Lorem @file ");
2649            assert!(editor.has_visible_completions_menu());
2650        });
2651
2652        cx.simulate_input("one");
2653
2654        editor.update(&mut cx, |editor, cx| {
2655            assert_eq!(editor.text(cx), "Lorem @file one");
2656            assert!(editor.has_visible_completions_menu());
2657            assert_eq!(
2658                current_completion_labels(editor),
2659                vec![format!("one.txt a{slash}")]
2660            );
2661        });
2662
2663        editor.update_in(&mut cx, |editor, window, cx| {
2664            assert!(editor.has_visible_completions_menu());
2665            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2666        });
2667
2668        let url_one = MentionUri::File {
2669            abs_path: path!("/dir/a/one.txt").into(),
2670        }
2671        .to_uri()
2672        .to_string();
2673        editor.update(&mut cx, |editor, cx| {
2674            let text = editor.text(cx);
2675            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2676            assert!(!editor.has_visible_completions_menu());
2677            assert_eq!(fold_ranges(editor, cx).len(), 1);
2678        });
2679
2680        let contents = message_editor
2681            .update(&mut cx, |message_editor, cx| {
2682                message_editor
2683                    .mention_set()
2684                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2685            })
2686            .await
2687            .unwrap()
2688            .into_values()
2689            .collect::<Vec<_>>();
2690
2691        {
2692            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2693                panic!("Unexpected mentions");
2694            };
2695            pretty_assertions::assert_eq!(content, "1");
2696            pretty_assertions::assert_eq!(
2697                uri,
2698                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2699            );
2700        }
2701
2702        cx.simulate_input(" ");
2703
2704        editor.update(&mut cx, |editor, cx| {
2705            let text = editor.text(cx);
2706            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2707            assert!(!editor.has_visible_completions_menu());
2708            assert_eq!(fold_ranges(editor, cx).len(), 1);
2709        });
2710
2711        cx.simulate_input("Ipsum ");
2712
2713        editor.update(&mut cx, |editor, cx| {
2714            let text = editor.text(cx);
2715            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2716            assert!(!editor.has_visible_completions_menu());
2717            assert_eq!(fold_ranges(editor, cx).len(), 1);
2718        });
2719
2720        cx.simulate_input("@file ");
2721
2722        editor.update(&mut cx, |editor, cx| {
2723            let text = editor.text(cx);
2724            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2725            assert!(editor.has_visible_completions_menu());
2726            assert_eq!(fold_ranges(editor, cx).len(), 1);
2727        });
2728
2729        editor.update_in(&mut cx, |editor, window, cx| {
2730            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2731        });
2732
2733        cx.run_until_parked();
2734
2735        let contents = message_editor
2736            .update(&mut cx, |message_editor, cx| {
2737                message_editor
2738                    .mention_set()
2739                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2740            })
2741            .await
2742            .unwrap()
2743            .into_values()
2744            .collect::<Vec<_>>();
2745
2746        let url_eight = MentionUri::File {
2747            abs_path: path!("/dir/b/eight.txt").into(),
2748        }
2749        .to_uri()
2750        .to_string();
2751
2752        {
2753            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2754                panic!("Unexpected mentions");
2755            };
2756            pretty_assertions::assert_eq!(content, "8");
2757            pretty_assertions::assert_eq!(
2758                uri,
2759                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2760            );
2761        }
2762
2763        editor.update(&mut cx, |editor, cx| {
2764            assert_eq!(
2765                editor.text(cx),
2766                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2767            );
2768            assert!(!editor.has_visible_completions_menu());
2769            assert_eq!(fold_ranges(editor, cx).len(), 2);
2770        });
2771
2772        let plain_text_language = Arc::new(language::Language::new(
2773            language::LanguageConfig {
2774                name: "Plain Text".into(),
2775                matcher: language::LanguageMatcher {
2776                    path_suffixes: vec!["txt".to_string()],
2777                    ..Default::default()
2778                },
2779                ..Default::default()
2780            },
2781            None,
2782        ));
2783
2784        // Register the language and fake LSP
2785        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2786        language_registry.add(plain_text_language);
2787
2788        let mut fake_language_servers = language_registry.register_fake_lsp(
2789            "Plain Text",
2790            language::FakeLspAdapter {
2791                capabilities: lsp::ServerCapabilities {
2792                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2793                    ..Default::default()
2794                },
2795                ..Default::default()
2796            },
2797        );
2798
2799        // Open the buffer to trigger LSP initialization
2800        let buffer = project
2801            .update(&mut cx, |project, cx| {
2802                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2803            })
2804            .await
2805            .unwrap();
2806
2807        // Register the buffer with language servers
2808        let _handle = project.update(&mut cx, |project, cx| {
2809            project.register_buffer_with_language_servers(&buffer, cx)
2810        });
2811
2812        cx.run_until_parked();
2813
2814        let fake_language_server = fake_language_servers.next().await.unwrap();
2815        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2816            move |_, _| async move {
2817                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2818                    #[allow(deprecated)]
2819                    lsp::SymbolInformation {
2820                        name: "MySymbol".into(),
2821                        location: lsp::Location {
2822                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2823                            range: lsp::Range::new(
2824                                lsp::Position::new(0, 0),
2825                                lsp::Position::new(0, 1),
2826                            ),
2827                        },
2828                        kind: lsp::SymbolKind::CONSTANT,
2829                        tags: None,
2830                        container_name: None,
2831                        deprecated: None,
2832                    },
2833                ])))
2834            },
2835        );
2836
2837        cx.simulate_input("@symbol ");
2838
2839        editor.update(&mut cx, |editor, cx| {
2840            assert_eq!(
2841                editor.text(cx),
2842                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2843            );
2844            assert!(editor.has_visible_completions_menu());
2845            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2846        });
2847
2848        editor.update_in(&mut cx, |editor, window, cx| {
2849            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2850        });
2851
2852        let symbol = MentionUri::Symbol {
2853            abs_path: path!("/dir/a/one.txt").into(),
2854            name: "MySymbol".into(),
2855            line_range: 0..=0,
2856        };
2857
2858        let contents = message_editor
2859            .update(&mut cx, |message_editor, cx| {
2860                message_editor
2861                    .mention_set()
2862                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2863            })
2864            .await
2865            .unwrap()
2866            .into_values()
2867            .collect::<Vec<_>>();
2868
2869        {
2870            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2871                panic!("Unexpected mentions");
2872            };
2873            pretty_assertions::assert_eq!(content, "1");
2874            pretty_assertions::assert_eq!(uri, &symbol);
2875        }
2876
2877        cx.run_until_parked();
2878
2879        editor.read_with(&cx, |editor, cx| {
2880            assert_eq!(
2881                editor.text(cx),
2882                format!(
2883                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2884                    symbol.to_uri(),
2885                )
2886            );
2887        });
2888
2889        // Try to mention an "image" file that will fail to load
2890        cx.simulate_input("@file x.png");
2891
2892        editor.update(&mut cx, |editor, cx| {
2893            assert_eq!(
2894                editor.text(cx),
2895                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2896            );
2897            assert!(editor.has_visible_completions_menu());
2898            assert_eq!(current_completion_labels(editor), &["x.png "]);
2899        });
2900
2901        editor.update_in(&mut cx, |editor, window, cx| {
2902            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2903        });
2904
2905        // Getting the message contents fails
2906        message_editor
2907            .update(&mut cx, |message_editor, cx| {
2908                message_editor
2909                    .mention_set()
2910                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2911            })
2912            .await
2913            .expect_err("Should fail to load x.png");
2914
2915        cx.run_until_parked();
2916
2917        // Mention was removed
2918        editor.read_with(&cx, |editor, cx| {
2919            assert_eq!(
2920                editor.text(cx),
2921                format!(
2922                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2923                    symbol.to_uri()
2924                )
2925            );
2926        });
2927
2928        // Once more
2929        cx.simulate_input("@file x.png");
2930
2931        editor.update(&mut cx, |editor, cx| {
2932                    assert_eq!(
2933                        editor.text(cx),
2934                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2935                    );
2936                    assert!(editor.has_visible_completions_menu());
2937                    assert_eq!(current_completion_labels(editor), &["x.png "]);
2938                });
2939
2940        editor.update_in(&mut cx, |editor, window, cx| {
2941            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2942        });
2943
2944        // This time don't immediately get the contents, just let the confirmed completion settle
2945        cx.run_until_parked();
2946
2947        // Mention was removed
2948        editor.read_with(&cx, |editor, cx| {
2949            assert_eq!(
2950                editor.text(cx),
2951                format!(
2952                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2953                    symbol.to_uri()
2954                )
2955            );
2956        });
2957
2958        // Now getting the contents succeeds, because the invalid mention was removed
2959        let contents = message_editor
2960            .update(&mut cx, |message_editor, cx| {
2961                message_editor
2962                    .mention_set()
2963                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2964            })
2965            .await
2966            .unwrap();
2967        assert_eq!(contents.len(), 3);
2968    }
2969
2970    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2971        let snapshot = editor.buffer().read(cx).snapshot(cx);
2972        editor.display_map.update(cx, |display_map, cx| {
2973            display_map
2974                .snapshot(cx)
2975                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
2976                .map(|fold| fold.range.to_point(&snapshot))
2977                .collect()
2978        })
2979    }
2980
2981    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2982        let completions = editor.current_completions().expect("Missing completions");
2983        completions
2984            .into_iter()
2985            .map(|completion| completion.label.text)
2986            .collect::<Vec<_>>()
2987    }
2988
2989    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2990        let completions = editor.current_completions().expect("Missing completions");
2991        completions
2992            .into_iter()
2993            .map(|completion| {
2994                (
2995                    completion.label.text,
2996                    completion
2997                        .documentation
2998                        .map(|d| d.text().to_string())
2999                        .unwrap_or_default(),
3000                )
3001            })
3002            .collect::<Vec<_>>()
3003    }
3004
3005    #[gpui::test]
3006    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
3007        init_test(cx);
3008
3009        let fs = FakeFs::new(cx.executor());
3010
3011        // Create a large file that exceeds AUTO_OUTLINE_SIZE
3012        // Using plain text without a configured language, so no outline is available
3013        const LINE: &str = "This is a line of text in the file\n";
3014        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
3015        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
3016
3017        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
3018        let small_content = "fn small_function() { /* small */ }\n";
3019        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
3020
3021        fs.insert_tree(
3022            "/project",
3023            json!({
3024                "large_file.txt": large_content.clone(),
3025                "small_file.txt": small_content,
3026            }),
3027        )
3028        .await;
3029
3030        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3031
3032        let (multi_workspace, cx) =
3033            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3034        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3035
3036        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3037
3038        let message_editor = cx.update(|window, cx| {
3039            cx.new(|cx| {
3040                let editor = MessageEditor::new(
3041                    workspace.downgrade(),
3042                    project.downgrade(),
3043                    thread_store.clone(),
3044                    None,
3045                    None,
3046                    Default::default(),
3047                    "Test Agent".into(),
3048                    "Test",
3049                    EditorMode::AutoHeight {
3050                        min_lines: 1,
3051                        max_lines: None,
3052                    },
3053                    window,
3054                    cx,
3055                );
3056                // Enable embedded context so files are actually included
3057                editor
3058                    .session_capabilities
3059                    .write()
3060                    .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3061                editor
3062            })
3063        });
3064
3065        // Test large file mention
3066        // Get the absolute path using the project's worktree
3067        let large_file_abs_path = project.read_with(cx, |project, cx| {
3068            let worktree = project.worktrees(cx).next().unwrap();
3069            let worktree_root = worktree.read(cx).abs_path();
3070            worktree_root.join("large_file.txt")
3071        });
3072        let large_file_task = message_editor.update(cx, |editor, cx| {
3073            editor.mention_set().update(cx, |set, cx| {
3074                set.confirm_mention_for_file(large_file_abs_path, true, cx)
3075            })
3076        });
3077
3078        let large_file_mention = large_file_task.await.unwrap();
3079        match large_file_mention {
3080            Mention::Text { content, .. } => {
3081                // Should contain some of the content but not all of it
3082                assert!(
3083                    content.contains(LINE),
3084                    "Should contain some of the file content"
3085                );
3086                assert!(
3087                    !content.contains(&LINE.repeat(100)),
3088                    "Should not contain the full file"
3089                );
3090                // Should be much smaller than original
3091                assert!(
3092                    content.len() < large_content.len() / 10,
3093                    "Should be significantly truncated"
3094                );
3095            }
3096            _ => panic!("Expected Text mention for large file"),
3097        }
3098
3099        // Test small file mention
3100        // Get the absolute path using the project's worktree
3101        let small_file_abs_path = project.read_with(cx, |project, cx| {
3102            let worktree = project.worktrees(cx).next().unwrap();
3103            let worktree_root = worktree.read(cx).abs_path();
3104            worktree_root.join("small_file.txt")
3105        });
3106        let small_file_task = message_editor.update(cx, |editor, cx| {
3107            editor.mention_set().update(cx, |set, cx| {
3108                set.confirm_mention_for_file(small_file_abs_path, true, cx)
3109            })
3110        });
3111
3112        let small_file_mention = small_file_task.await.unwrap();
3113        match small_file_mention {
3114            Mention::Text { content, .. } => {
3115                // Should contain the full actual content
3116                assert_eq!(content, small_content);
3117            }
3118            _ => panic!("Expected Text mention for small file"),
3119        }
3120    }
3121
3122    #[gpui::test]
3123    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
3124        init_test(cx);
3125        cx.update(LanguageModelRegistry::test);
3126
3127        let fs = FakeFs::new(cx.executor());
3128        fs.insert_tree("/project", json!({"file": ""})).await;
3129        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3130
3131        let (multi_workspace, cx) =
3132            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3133        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3134
3135        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3136
3137        let session_id = acp::SessionId::new("thread-123");
3138        let title = Some("Previous Conversation".into());
3139
3140        let message_editor = cx.update(|window, cx| {
3141            cx.new(|cx| {
3142                let mut editor = MessageEditor::new(
3143                    workspace.downgrade(),
3144                    project.downgrade(),
3145                    thread_store.clone(),
3146                    None,
3147                    None,
3148                    Default::default(),
3149                    "Test Agent".into(),
3150                    "Test",
3151                    EditorMode::AutoHeight {
3152                        min_lines: 1,
3153                        max_lines: None,
3154                    },
3155                    window,
3156                    cx,
3157                );
3158                editor.insert_thread_summary(session_id.clone(), title.clone(), window, cx);
3159                editor
3160            })
3161        });
3162
3163        // Construct expected values for verification
3164        let expected_uri = MentionUri::Thread {
3165            id: session_id.clone(),
3166            name: title.as_ref().unwrap().to_string(),
3167        };
3168        let expected_title = title.as_ref().unwrap();
3169        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
3170
3171        message_editor.read_with(cx, |editor, cx| {
3172            let text = editor.text(cx);
3173
3174            assert!(
3175                text.contains(&expected_link),
3176                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
3177                expected_link,
3178                text
3179            );
3180
3181            let mentions = editor.mention_set().read(cx).mentions();
3182            assert_eq!(
3183                mentions.len(),
3184                1,
3185                "Expected exactly one mention after inserting thread summary"
3186            );
3187
3188            assert!(
3189                mentions.contains(&expected_uri),
3190                "Expected mentions to contain the thread URI"
3191            );
3192        });
3193    }
3194
3195    #[gpui::test]
3196    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
3197        init_test(cx);
3198        cx.update(LanguageModelRegistry::test);
3199
3200        let fs = FakeFs::new(cx.executor());
3201        fs.insert_tree("/project", json!({"file": ""})).await;
3202        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3203
3204        let (multi_workspace, cx) =
3205            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3206        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3207
3208        let thread_store = None;
3209
3210        let message_editor = cx.update(|window, cx| {
3211            cx.new(|cx| {
3212                let mut editor = MessageEditor::new(
3213                    workspace.downgrade(),
3214                    project.downgrade(),
3215                    thread_store.clone(),
3216                    None,
3217                    None,
3218                    Default::default(),
3219                    "Test Agent".into(),
3220                    "Test",
3221                    EditorMode::AutoHeight {
3222                        min_lines: 1,
3223                        max_lines: None,
3224                    },
3225                    window,
3226                    cx,
3227                );
3228                editor.insert_thread_summary(
3229                    acp::SessionId::new("thread-123"),
3230                    Some("Previous Conversation".into()),
3231                    window,
3232                    cx,
3233                );
3234                editor
3235            })
3236        });
3237
3238        message_editor.read_with(cx, |editor, cx| {
3239            assert!(
3240                editor.text(cx).is_empty(),
3241                "Expected thread summary to be skipped for external agents"
3242            );
3243            assert!(
3244                editor.mention_set().read(cx).mentions().is_empty(),
3245                "Expected no mentions when thread summary is skipped"
3246            );
3247        });
3248    }
3249
3250    #[gpui::test]
3251    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
3252        init_test(cx);
3253
3254        let fs = FakeFs::new(cx.executor());
3255        fs.insert_tree("/project", json!({"file": ""})).await;
3256        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3257
3258        let (multi_workspace, cx) =
3259            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3260        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3261
3262        let thread_store = None;
3263
3264        let message_editor = cx.update(|window, cx| {
3265            cx.new(|cx| {
3266                MessageEditor::new(
3267                    workspace.downgrade(),
3268                    project.downgrade(),
3269                    thread_store.clone(),
3270                    None,
3271                    None,
3272                    Default::default(),
3273                    "Test Agent".into(),
3274                    "Test",
3275                    EditorMode::AutoHeight {
3276                        min_lines: 1,
3277                        max_lines: None,
3278                    },
3279                    window,
3280                    cx,
3281                )
3282            })
3283        });
3284
3285        message_editor.update(cx, |editor, _cx| {
3286            editor
3287                .session_capabilities
3288                .write()
3289                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3290        });
3291
3292        let supported_modes = {
3293            let app = cx.app.borrow();
3294            let _ = &app;
3295            message_editor
3296                .read(&app)
3297                .session_capabilities
3298                .read()
3299                .supported_modes(false)
3300        };
3301
3302        assert!(
3303            !supported_modes.contains(&PromptContextType::Thread),
3304            "Expected thread mode to be hidden when thread mentions are disabled"
3305        );
3306    }
3307
3308    #[gpui::test]
3309    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
3310        init_test(cx);
3311
3312        let fs = FakeFs::new(cx.executor());
3313        fs.insert_tree("/project", json!({"file": ""})).await;
3314        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3315
3316        let (multi_workspace, cx) =
3317            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3318        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3319
3320        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3321
3322        let message_editor = cx.update(|window, cx| {
3323            cx.new(|cx| {
3324                MessageEditor::new(
3325                    workspace.downgrade(),
3326                    project.downgrade(),
3327                    thread_store.clone(),
3328                    None,
3329                    None,
3330                    Default::default(),
3331                    "Test Agent".into(),
3332                    "Test",
3333                    EditorMode::AutoHeight {
3334                        min_lines: 1,
3335                        max_lines: None,
3336                    },
3337                    window,
3338                    cx,
3339                )
3340            })
3341        });
3342
3343        message_editor.update(cx, |editor, _cx| {
3344            editor
3345                .session_capabilities
3346                .write()
3347                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3348        });
3349
3350        let supported_modes = {
3351            let app = cx.app.borrow();
3352            let _ = &app;
3353            message_editor
3354                .read(&app)
3355                .session_capabilities
3356                .read()
3357                .supported_modes(true)
3358        };
3359
3360        assert!(
3361            supported_modes.contains(&PromptContextType::Thread),
3362            "Expected thread mode to be visible when enabled"
3363        );
3364    }
3365
3366    #[gpui::test]
3367    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
3368        init_test(cx);
3369
3370        let fs = FakeFs::new(cx.executor());
3371        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
3372            .await;
3373        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3374
3375        let (multi_workspace, cx) =
3376            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3377        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3378
3379        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3380
3381        let message_editor = cx.update(|window, cx| {
3382            cx.new(|cx| {
3383                MessageEditor::new(
3384                    workspace.downgrade(),
3385                    project.downgrade(),
3386                    thread_store.clone(),
3387                    None,
3388                    None,
3389                    Default::default(),
3390                    "Test Agent".into(),
3391                    "Test",
3392                    EditorMode::AutoHeight {
3393                        min_lines: 1,
3394                        max_lines: None,
3395                    },
3396                    window,
3397                    cx,
3398                )
3399            })
3400        });
3401        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
3402
3403        cx.run_until_parked();
3404
3405        editor.update_in(cx, |editor, window, cx| {
3406            editor.set_text("  \u{A0}してhello world  ", window, cx);
3407        });
3408
3409        let (content, _) = message_editor
3410            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
3411            .await
3412            .unwrap();
3413
3414        assert_eq!(content, vec!["してhello world".into()]);
3415    }
3416
3417    #[gpui::test]
3418    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
3419        init_test(cx);
3420
3421        let fs = FakeFs::new(cx.executor());
3422
3423        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
3424
3425        fs.insert_tree(
3426            "/project",
3427            json!({
3428                "src": {
3429                    "main.rs": file_content,
3430                }
3431            }),
3432        )
3433        .await;
3434
3435        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3436
3437        let (multi_workspace, cx) =
3438            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3439        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3440
3441        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3442
3443        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
3444            let workspace_handle = cx.weak_entity();
3445            let message_editor = cx.new(|cx| {
3446                MessageEditor::new(
3447                    workspace_handle,
3448                    project.downgrade(),
3449                    thread_store.clone(),
3450                    None,
3451                    None,
3452                    Default::default(),
3453                    "Test Agent".into(),
3454                    "Test",
3455                    EditorMode::AutoHeight {
3456                        min_lines: 1,
3457                        max_lines: None,
3458                    },
3459                    window,
3460                    cx,
3461                )
3462            });
3463            workspace.active_pane().update(cx, |pane, cx| {
3464                pane.add_item(
3465                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3466                    true,
3467                    true,
3468                    None,
3469                    window,
3470                    cx,
3471                );
3472            });
3473            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3474            let editor = message_editor.read(cx).editor().clone();
3475            (message_editor, editor)
3476        });
3477
3478        cx.simulate_input("What is in @file main");
3479
3480        editor.update_in(cx, |editor, window, cx| {
3481            assert!(editor.has_visible_completions_menu());
3482            assert_eq!(editor.text(cx), "What is in @file main");
3483            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
3484        });
3485
3486        let content = message_editor
3487            .update(cx, |editor, cx| editor.contents(false, cx))
3488            .await
3489            .unwrap()
3490            .0;
3491
3492        let main_rs_uri = if cfg!(windows) {
3493            "file:///C:/project/src/main.rs"
3494        } else {
3495            "file:///project/src/main.rs"
3496        };
3497
3498        // When embedded context is `false` we should get a resource link
3499        pretty_assertions::assert_eq!(
3500            content,
3501            vec![
3502                "What is in ".into(),
3503                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
3504            ]
3505        );
3506
3507        message_editor.update(cx, |editor, _cx| {
3508            editor
3509                .session_capabilities
3510                .write()
3511                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true))
3512        });
3513
3514        let content = message_editor
3515            .update(cx, |editor, cx| editor.contents(false, cx))
3516            .await
3517            .unwrap()
3518            .0;
3519
3520        // When embedded context is `true` we should get a resource
3521        pretty_assertions::assert_eq!(
3522            content,
3523            vec![
3524                "What is in ".into(),
3525                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
3526                    acp::EmbeddedResourceResource::TextResourceContents(
3527                        acp::TextResourceContents::new(file_content, main_rs_uri)
3528                    )
3529                ))
3530            ]
3531        );
3532    }
3533
3534    #[gpui::test]
3535    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
3536        init_test(cx);
3537
3538        let app_state = cx.update(AppState::test);
3539
3540        cx.update(|cx| {
3541            editor::init(cx);
3542            workspace::init(app_state.clone(), cx);
3543        });
3544
3545        app_state
3546            .fs
3547            .as_fake()
3548            .insert_tree(
3549                path!("/dir"),
3550                json!({
3551                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
3552                }),
3553            )
3554            .await;
3555
3556        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3557        let window =
3558            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3559        let workspace = window
3560            .read_with(cx, |mw, _| mw.workspace().clone())
3561            .unwrap();
3562
3563        let worktree = project.update(cx, |project, cx| {
3564            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
3565            assert_eq!(worktrees.len(), 1);
3566            worktrees.pop().unwrap()
3567        });
3568        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
3569
3570        let mut cx = VisualTestContext::from_window(window.into(), cx);
3571
3572        // Open a regular editor with the created file, and select a portion of
3573        // the text that will be used for the selections that are meant to be
3574        // inserted in the agent panel.
3575        let editor = workspace
3576            .update_in(&mut cx, |workspace, window, cx| {
3577                workspace.open_path(
3578                    ProjectPath {
3579                        worktree_id,
3580                        path: rel_path("test.txt").into(),
3581                    },
3582                    None,
3583                    false,
3584                    window,
3585                    cx,
3586                )
3587            })
3588            .await
3589            .unwrap()
3590            .downcast::<Editor>()
3591            .unwrap();
3592
3593        editor.update_in(&mut cx, |editor, window, cx| {
3594            editor.change_selections(Default::default(), window, cx, |selections| {
3595                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
3596            });
3597        });
3598
3599        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3600
3601        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
3602        // to ensure we have a fixed viewport, so we can eventually actually
3603        // place the cursor outside of the visible area.
3604        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
3605            let workspace_handle = cx.weak_entity();
3606            let message_editor = cx.new(|cx| {
3607                MessageEditor::new(
3608                    workspace_handle,
3609                    project.downgrade(),
3610                    thread_store.clone(),
3611                    None,
3612                    None,
3613                    Default::default(),
3614                    "Test Agent".into(),
3615                    "Test",
3616                    EditorMode::full(),
3617                    window,
3618                    cx,
3619                )
3620            });
3621            workspace.active_pane().update(cx, |pane, cx| {
3622                pane.add_item(
3623                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3624                    true,
3625                    true,
3626                    None,
3627                    window,
3628                    cx,
3629                );
3630            });
3631
3632            message_editor
3633        });
3634
3635        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3636            message_editor.editor.update(cx, |editor, cx| {
3637                // Update the Agent Panel's Message Editor text to have 100
3638                // lines, ensuring that the cursor is set at line 90 and that we
3639                // then scroll all the way to the top, so the cursor's position
3640                // remains off screen.
3641                let mut lines = String::new();
3642                for _ in 1..=100 {
3643                    lines.push_str(&"Another line in the agent panel's message editor\n");
3644                }
3645                editor.set_text(lines.as_str(), window, cx);
3646                editor.change_selections(Default::default(), window, cx, |selections| {
3647                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
3648                });
3649                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
3650            });
3651        });
3652
3653        cx.run_until_parked();
3654
3655        // Before proceeding, let's assert that the cursor is indeed off screen,
3656        // otherwise the rest of the test doesn't make sense.
3657        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3658            message_editor.editor.update(cx, |editor, cx| {
3659                let snapshot = editor.snapshot(window, cx);
3660                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3661                let scroll_top = snapshot.scroll_position().y as u32;
3662                let visible_lines = editor.visible_line_count().unwrap() as u32;
3663                let visible_range = scroll_top..(scroll_top + visible_lines);
3664
3665                assert!(!visible_range.contains(&cursor_row));
3666            })
3667        });
3668
3669        // Now let's insert the selection in the Agent Panel's editor and
3670        // confirm that, after the insertion, the cursor is now in the visible
3671        // range.
3672        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3673            message_editor.insert_selections(window, cx);
3674        });
3675
3676        cx.run_until_parked();
3677
3678        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3679            message_editor.editor.update(cx, |editor, cx| {
3680                let snapshot = editor.snapshot(window, cx);
3681                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3682                let scroll_top = snapshot.scroll_position().y as u32;
3683                let visible_lines = editor.visible_line_count().unwrap() as u32;
3684                let visible_range = scroll_top..(scroll_top + visible_lines);
3685
3686                assert!(visible_range.contains(&cursor_row));
3687            })
3688        });
3689    }
3690
3691    #[gpui::test]
3692    async fn test_insert_context_with_multibyte_characters(cx: &mut TestAppContext) {
3693        init_test(cx);
3694
3695        let app_state = cx.update(AppState::test);
3696
3697        cx.update(|cx| {
3698            editor::init(cx);
3699            workspace::init(app_state.clone(), cx);
3700        });
3701
3702        app_state
3703            .fs
3704            .as_fake()
3705            .insert_tree(path!("/dir"), json!({}))
3706            .await;
3707
3708        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3709        let window =
3710            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3711        let workspace = window
3712            .read_with(cx, |mw, _| mw.workspace().clone())
3713            .unwrap();
3714
3715        let mut cx = VisualTestContext::from_window(window.into(), cx);
3716
3717        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3718
3719        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3720            let workspace_handle = cx.weak_entity();
3721            let message_editor = cx.new(|cx| {
3722                MessageEditor::new(
3723                    workspace_handle,
3724                    project.downgrade(),
3725                    Some(thread_store.clone()),
3726                    None,
3727                    None,
3728                    Default::default(),
3729                    "Test Agent".into(),
3730                    "Test",
3731                    EditorMode::AutoHeight {
3732                        max_lines: None,
3733                        min_lines: 1,
3734                    },
3735                    window,
3736                    cx,
3737                )
3738            });
3739            workspace.active_pane().update(cx, |pane, cx| {
3740                pane.add_item(
3741                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3742                    true,
3743                    true,
3744                    None,
3745                    window,
3746                    cx,
3747                );
3748            });
3749            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3750            let editor = message_editor.read(cx).editor().clone();
3751            (message_editor, editor)
3752        });
3753
3754        editor.update_in(&mut cx, |editor, window, cx| {
3755            editor.set_text("😄😄", window, cx);
3756        });
3757
3758        cx.run_until_parked();
3759
3760        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3761            message_editor.insert_context_type("file", window, cx);
3762        });
3763
3764        cx.run_until_parked();
3765
3766        editor.update(&mut cx, |editor, cx| {
3767            assert_eq!(editor.text(cx), "😄😄@file");
3768        });
3769    }
3770
3771    #[gpui::test]
3772    async fn test_paste_mention_link_with_multiple_selections(cx: &mut TestAppContext) {
3773        init_test(cx);
3774
3775        let app_state = cx.update(AppState::test);
3776
3777        cx.update(|cx| {
3778            editor::init(cx);
3779            workspace::init(app_state.clone(), cx);
3780        });
3781
3782        app_state
3783            .fs
3784            .as_fake()
3785            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3786            .await;
3787
3788        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3789        let window =
3790            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3791        let workspace = window
3792            .read_with(cx, |mw, _| mw.workspace().clone())
3793            .unwrap();
3794
3795        let mut cx = VisualTestContext::from_window(window.into(), cx);
3796
3797        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3798
3799        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3800            let workspace_handle = cx.weak_entity();
3801            let message_editor = cx.new(|cx| {
3802                MessageEditor::new(
3803                    workspace_handle,
3804                    project.downgrade(),
3805                    Some(thread_store),
3806                    None,
3807                    None,
3808                    Default::default(),
3809                    "Test Agent".into(),
3810                    "Test",
3811                    EditorMode::AutoHeight {
3812                        max_lines: None,
3813                        min_lines: 1,
3814                    },
3815                    window,
3816                    cx,
3817                )
3818            });
3819            workspace.active_pane().update(cx, |pane, cx| {
3820                pane.add_item(
3821                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3822                    true,
3823                    true,
3824                    None,
3825                    window,
3826                    cx,
3827                );
3828            });
3829            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3830            let editor = message_editor.read(cx).editor().clone();
3831            (message_editor, editor)
3832        });
3833
3834        editor.update_in(&mut cx, |editor, window, cx| {
3835            editor.set_text(
3836                "AAAAAAAAAAAAAAAAAAAAAAAAA     AAAAAAAAAAAAAAAAAAAAAAAAA",
3837                window,
3838                cx,
3839            );
3840        });
3841
3842        cx.run_until_parked();
3843
3844        editor.update_in(&mut cx, |editor, window, cx| {
3845            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3846                s.select_ranges([
3847                    MultiBufferOffset(0)..MultiBufferOffset(25), // First selection (large)
3848                    MultiBufferOffset(30)..MultiBufferOffset(55), // Second selection (newest)
3849                ]);
3850            });
3851        });
3852
3853        let mention_link = "[@f](file:///test.txt)";
3854        cx.write_to_clipboard(ClipboardItem::new_string(mention_link.into()));
3855
3856        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3857            message_editor.paste(&Paste, window, cx);
3858        });
3859
3860        let text = editor.update(&mut cx, |editor, cx| editor.text(cx));
3861        assert!(
3862            text.contains("[@f](file:///test.txt)"),
3863            "Expected mention link to be pasted, got: {}",
3864            text
3865        );
3866    }
3867
3868    #[gpui::test]
3869    async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
3870        cx: &mut TestAppContext,
3871    ) {
3872        init_test(cx);
3873
3874        let app_state = cx.update(AppState::test);
3875
3876        cx.update(|cx| {
3877            editor::init(cx);
3878            workspace::init(app_state.clone(), cx);
3879        });
3880
3881        app_state
3882            .fs
3883            .as_fake()
3884            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3885            .await;
3886
3887        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3888        let window =
3889            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3890        let workspace = window
3891            .read_with(cx, |mw, _| mw.workspace().clone())
3892            .unwrap();
3893
3894        let mut cx = VisualTestContext::from_window(window.into(), cx);
3895
3896        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3897
3898        let (_message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3899            let workspace_handle = cx.weak_entity();
3900            let message_editor = cx.new(|cx| {
3901                MessageEditor::new(
3902                    workspace_handle,
3903                    project.downgrade(),
3904                    Some(thread_store),
3905                    None,
3906                    None,
3907                    Default::default(),
3908                    "Test Agent".into(),
3909                    "Test",
3910                    EditorMode::AutoHeight {
3911                        max_lines: None,
3912                        min_lines: 1,
3913                    },
3914                    window,
3915                    cx,
3916                )
3917            });
3918            workspace.active_pane().update(cx, |pane, cx| {
3919                pane.add_item(
3920                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3921                    true,
3922                    true,
3923                    None,
3924                    window,
3925                    cx,
3926                );
3927            });
3928            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3929            let editor = message_editor.read(cx).editor().clone();
3930            (message_editor, editor)
3931        });
3932
3933        cx.simulate_input("@");
3934
3935        editor.update(&mut cx, |editor, cx| {
3936            assert_eq!(editor.text(cx), "@");
3937            assert!(editor.has_visible_completions_menu());
3938        });
3939
3940        cx.write_to_clipboard(ClipboardItem::new_string("[@f](file:///test.txt) @".into()));
3941        cx.dispatch_action(Paste);
3942
3943        editor.update(&mut cx, |editor, cx| {
3944            assert!(editor.text(cx).contains("[@f](file:///test.txt)"));
3945        });
3946    }
3947
3948    #[gpui::test]
3949    async fn test_paste_external_file_path_inserts_file_mention(cx: &mut TestAppContext) {
3950        init_test(cx);
3951        let (message_editor, editor, mut cx) =
3952            setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await;
3953        paste_external_paths(
3954            &message_editor,
3955            vec![PathBuf::from(path!("/project/file.txt"))],
3956            &mut cx,
3957        );
3958
3959        let expected_uri = MentionUri::File {
3960            abs_path: path!("/project/file.txt").into(),
3961        }
3962        .to_uri()
3963        .to_string();
3964
3965        editor.update(&mut cx, |editor, cx| {
3966            assert_eq!(editor.text(cx), format!("[@file.txt]({expected_uri}) "));
3967        });
3968
3969        let contents = mention_contents(&message_editor, &mut cx).await;
3970
3971        let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
3972            panic!("Unexpected mentions");
3973        };
3974        assert_eq!(content, "content");
3975        assert_eq!(
3976            uri,
3977            &MentionUri::File {
3978                abs_path: path!("/project/file.txt").into(),
3979            }
3980        );
3981    }
3982
3983    #[gpui::test]
3984    async fn test_paste_external_directory_path_inserts_directory_mention(cx: &mut TestAppContext) {
3985        init_test(cx);
3986        let (message_editor, editor, mut cx) = setup_paste_test_message_editor(
3987            json!({
3988                "src": {
3989                    "main.rs": "fn main() {}\n",
3990                }
3991            }),
3992            cx,
3993        )
3994        .await;
3995        paste_external_paths(
3996            &message_editor,
3997            vec![PathBuf::from(path!("/project/src"))],
3998            &mut cx,
3999        );
4000
4001        let expected_uri = MentionUri::Directory {
4002            abs_path: path!("/project/src").into(),
4003        }
4004        .to_uri()
4005        .to_string();
4006
4007        editor.update(&mut cx, |editor, cx| {
4008            assert_eq!(editor.text(cx), format!("[@src]({expected_uri}) "));
4009        });
4010
4011        let contents = mention_contents(&message_editor, &mut cx).await;
4012
4013        let [(uri, Mention::Link)] = contents.as_slice() else {
4014            panic!("Unexpected mentions");
4015        };
4016        assert_eq!(
4017            uri,
4018            &MentionUri::Directory {
4019                abs_path: path!("/project/src").into(),
4020            }
4021        );
4022    }
4023
4024    #[gpui::test]
4025    async fn test_paste_external_file_path_inserts_at_cursor(cx: &mut TestAppContext) {
4026        init_test(cx);
4027        let (message_editor, editor, mut cx) =
4028            setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await;
4029
4030        editor.update_in(&mut cx, |editor, window, cx| {
4031            editor.set_text("Hello world", window, cx);
4032            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
4033                selections.select_ranges([MultiBufferOffset(6)..MultiBufferOffset(6)]);
4034            });
4035        });
4036
4037        paste_external_paths(
4038            &message_editor,
4039            vec![PathBuf::from(path!("/project/file.txt"))],
4040            &mut cx,
4041        );
4042
4043        let expected_uri = MentionUri::File {
4044            abs_path: path!("/project/file.txt").into(),
4045        }
4046        .to_uri()
4047        .to_string();
4048
4049        editor.update(&mut cx, |editor, cx| {
4050            assert_eq!(
4051                editor.text(cx),
4052                format!("Hello [@file.txt]({expected_uri}) world")
4053            );
4054        });
4055    }
4056
4057    #[gpui::test]
4058    async fn test_paste_mixed_external_image_without_extension_and_file_path(
4059        cx: &mut TestAppContext,
4060    ) {
4061        init_test(cx);
4062        let (message_editor, editor, mut cx) =
4063            setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await;
4064
4065        message_editor.update(&mut cx, |message_editor, _cx| {
4066            message_editor
4067                .session_capabilities
4068                .write()
4069                .set_prompt_capabilities(acp::PromptCapabilities::new().image(true));
4070        });
4071
4072        let temporary_image_path = write_test_png_file(None);
4073        paste_external_paths(
4074            &message_editor,
4075            vec![
4076                temporary_image_path.clone(),
4077                PathBuf::from(path!("/project/file.txt")),
4078            ],
4079            &mut cx,
4080        );
4081
4082        let image_name = temporary_image_path
4083            .file_name()
4084            .and_then(|n| n.to_str())
4085            .unwrap_or("Image")
4086            .to_string();
4087        std::fs::remove_file(&temporary_image_path).expect("remove temp png");
4088
4089        let expected_file_uri = MentionUri::File {
4090            abs_path: path!("/project/file.txt").into(),
4091        }
4092        .to_uri()
4093        .to_string();
4094        let expected_image_uri = MentionUri::PastedImage {
4095            name: image_name.clone(),
4096        }
4097        .to_uri()
4098        .to_string();
4099
4100        editor.update(&mut cx, |editor, cx| {
4101            assert_eq!(
4102                editor.text(cx),
4103                format!("[@{image_name}]({expected_image_uri}) [@file.txt]({expected_file_uri}) ")
4104            );
4105        });
4106
4107        let contents = mention_contents(&message_editor, &mut cx).await;
4108
4109        assert_eq!(contents.len(), 2);
4110        assert!(contents.iter().any(|(uri, mention)| {
4111            matches!(uri, MentionUri::PastedImage { .. }) && matches!(mention, Mention::Image(_))
4112        }));
4113        assert!(contents.iter().any(|(uri, mention)| {
4114            *uri == MentionUri::File {
4115                abs_path: path!("/project/file.txt").into(),
4116            } && matches!(
4117                mention,
4118                Mention::Text {
4119                    content,
4120                    tracked_buffers: _,
4121                } if content == "content"
4122            )
4123        }));
4124    }
4125
4126    async fn setup_paste_test_message_editor(
4127        project_tree: Value,
4128        cx: &mut TestAppContext,
4129    ) -> (Entity<MessageEditor>, Entity<Editor>, VisualTestContext) {
4130        let app_state = cx.update(AppState::test);
4131
4132        cx.update(|cx| {
4133            editor::init(cx);
4134            workspace::init(app_state.clone(), cx);
4135        });
4136
4137        app_state
4138            .fs
4139            .as_fake()
4140            .insert_tree(path!("/project"), project_tree)
4141            .await;
4142
4143        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
4144        let window =
4145            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4146        let workspace = window
4147            .read_with(cx, |mw, _| mw.workspace().clone())
4148            .unwrap();
4149
4150        let mut cx = VisualTestContext::from_window(window.into(), cx);
4151
4152        let thread_store = cx.new(|cx| ThreadStore::new(cx));
4153
4154        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
4155            let workspace_handle = cx.weak_entity();
4156            let message_editor = cx.new(|cx| {
4157                MessageEditor::new(
4158                    workspace_handle,
4159                    project.downgrade(),
4160                    Some(thread_store),
4161                    None,
4162                    None,
4163                    Default::default(),
4164                    "Test Agent".into(),
4165                    "Test",
4166                    EditorMode::AutoHeight {
4167                        max_lines: None,
4168                        min_lines: 1,
4169                    },
4170                    window,
4171                    cx,
4172                )
4173            });
4174            workspace.active_pane().update(cx, |pane, cx| {
4175                pane.add_item(
4176                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
4177                    true,
4178                    true,
4179                    None,
4180                    window,
4181                    cx,
4182                );
4183            });
4184            message_editor.read(cx).focus_handle(cx).focus(window, cx);
4185            let editor = message_editor.read(cx).editor().clone();
4186            (message_editor, editor)
4187        });
4188
4189        (message_editor, editor, cx)
4190    }
4191
4192    fn paste_external_paths(
4193        message_editor: &Entity<MessageEditor>,
4194        paths: Vec<PathBuf>,
4195        cx: &mut VisualTestContext,
4196    ) {
4197        cx.write_to_clipboard(ClipboardItem {
4198            entries: vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths.into()))],
4199        });
4200
4201        message_editor.update_in(cx, |message_editor, window, cx| {
4202            message_editor.paste(&Paste, window, cx);
4203        });
4204        cx.run_until_parked();
4205    }
4206
4207    async fn mention_contents(
4208        message_editor: &Entity<MessageEditor>,
4209        cx: &mut VisualTestContext,
4210    ) -> Vec<(MentionUri, Mention)> {
4211        message_editor
4212            .update(cx, |message_editor, cx| {
4213                message_editor
4214                    .mention_set()
4215                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
4216            })
4217            .await
4218            .unwrap()
4219            .into_values()
4220            .collect::<Vec<_>>()
4221    }
4222
4223    fn write_test_png_file(extension: Option<&str>) -> PathBuf {
4224        let bytes = base64::prelude::BASE64_STANDARD
4225            .decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
4226            .expect("decode png");
4227        let file_name = match extension {
4228            Some(extension) => format!("zed-agent-ui-test-{}.{}", uuid::Uuid::new_v4(), extension),
4229            None => format!("zed-agent-ui-test-{}", uuid::Uuid::new_v4()),
4230        };
4231        let path = std::env::temp_dir().join(file_name);
4232        std::fs::write(&path, bytes).expect("write temp png");
4233        path
4234    }
4235
4236    // Helper that creates a minimal MessageEditor inside a window, returning both
4237    // the entity and the underlying VisualTestContext so callers can drive updates.
4238    async fn setup_message_editor(
4239        cx: &mut TestAppContext,
4240    ) -> (Entity<MessageEditor>, &mut VisualTestContext) {
4241        let fs = FakeFs::new(cx.executor());
4242        fs.insert_tree("/project", json!({"file.txt": ""})).await;
4243        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
4244
4245        let (multi_workspace, cx) =
4246            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4247        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4248
4249        let message_editor = cx.update(|window, cx| {
4250            cx.new(|cx| {
4251                MessageEditor::new(
4252                    workspace.downgrade(),
4253                    project.downgrade(),
4254                    None,
4255                    None,
4256                    None,
4257                    Default::default(),
4258                    "Test Agent".into(),
4259                    "Test",
4260                    EditorMode::AutoHeight {
4261                        min_lines: 1,
4262                        max_lines: None,
4263                    },
4264                    window,
4265                    cx,
4266                )
4267            })
4268        });
4269
4270        cx.run_until_parked();
4271        (message_editor, cx)
4272    }
4273
4274    #[gpui::test]
4275    async fn test_set_message_plain_text(cx: &mut TestAppContext) {
4276        init_test(cx);
4277        let (message_editor, cx) = setup_message_editor(cx).await;
4278
4279        message_editor.update_in(cx, |editor, window, cx| {
4280            editor.set_message(
4281                vec![acp::ContentBlock::Text(acp::TextContent::new(
4282                    "hello world".to_string(),
4283                ))],
4284                window,
4285                cx,
4286            );
4287        });
4288
4289        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4290        assert_eq!(text, "hello world");
4291        assert!(!message_editor.update(cx, |editor, cx| editor.is_empty(cx)));
4292    }
4293
4294    #[gpui::test]
4295    async fn test_set_message_replaces_existing_content(cx: &mut TestAppContext) {
4296        init_test(cx);
4297        let (message_editor, cx) = setup_message_editor(cx).await;
4298
4299        // Set initial content.
4300        message_editor.update_in(cx, |editor, window, cx| {
4301            editor.set_message(
4302                vec![acp::ContentBlock::Text(acp::TextContent::new(
4303                    "old content".to_string(),
4304                ))],
4305                window,
4306                cx,
4307            );
4308        });
4309
4310        // Replace with new content.
4311        message_editor.update_in(cx, |editor, window, cx| {
4312            editor.set_message(
4313                vec![acp::ContentBlock::Text(acp::TextContent::new(
4314                    "new content".to_string(),
4315                ))],
4316                window,
4317                cx,
4318            );
4319        });
4320
4321        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4322        assert_eq!(
4323            text, "new content",
4324            "set_message should replace old content"
4325        );
4326    }
4327
4328    #[gpui::test]
4329    async fn test_append_message_to_empty_editor(cx: &mut TestAppContext) {
4330        init_test(cx);
4331        let (message_editor, cx) = setup_message_editor(cx).await;
4332
4333        message_editor.update_in(cx, |editor, window, cx| {
4334            editor.append_message(
4335                vec![acp::ContentBlock::Text(acp::TextContent::new(
4336                    "appended".to_string(),
4337                ))],
4338                Some("\n\n"),
4339                window,
4340                cx,
4341            );
4342        });
4343
4344        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4345        assert_eq!(
4346            text, "appended",
4347            "No separator should be inserted when the editor is empty"
4348        );
4349    }
4350
4351    #[gpui::test]
4352    async fn test_append_message_to_non_empty_editor(cx: &mut TestAppContext) {
4353        init_test(cx);
4354        let (message_editor, cx) = setup_message_editor(cx).await;
4355
4356        // Seed initial content.
4357        message_editor.update_in(cx, |editor, window, cx| {
4358            editor.set_message(
4359                vec![acp::ContentBlock::Text(acp::TextContent::new(
4360                    "initial".to_string(),
4361                ))],
4362                window,
4363                cx,
4364            );
4365        });
4366
4367        // Append with separator.
4368        message_editor.update_in(cx, |editor, window, cx| {
4369            editor.append_message(
4370                vec![acp::ContentBlock::Text(acp::TextContent::new(
4371                    "appended".to_string(),
4372                ))],
4373                Some("\n\n"),
4374                window,
4375                cx,
4376            );
4377        });
4378
4379        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4380        assert_eq!(
4381            text, "initial\n\nappended",
4382            "Separator should appear between existing and appended content"
4383        );
4384    }
4385
4386    #[gpui::test]
4387    async fn test_append_message_preserves_mention_offset(cx: &mut TestAppContext) {
4388        init_test(cx);
4389
4390        let fs = FakeFs::new(cx.executor());
4391        fs.insert_tree("/project", json!({"file.txt": "content"}))
4392            .await;
4393        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
4394
4395        let (multi_workspace, cx) =
4396            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4397        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4398
4399        let message_editor = cx.update(|window, cx| {
4400            cx.new(|cx| {
4401                MessageEditor::new(
4402                    workspace.downgrade(),
4403                    project.downgrade(),
4404                    None,
4405                    None,
4406                    None,
4407                    Default::default(),
4408                    "Test Agent".into(),
4409                    "Test",
4410                    EditorMode::AutoHeight {
4411                        min_lines: 1,
4412                        max_lines: None,
4413                    },
4414                    window,
4415                    cx,
4416                )
4417            })
4418        });
4419
4420        cx.run_until_parked();
4421
4422        // Seed plain-text prefix so the editor is non-empty before appending.
4423        message_editor.update_in(cx, |editor, window, cx| {
4424            editor.set_message(
4425                vec![acp::ContentBlock::Text(acp::TextContent::new(
4426                    "prefix text".to_string(),
4427                ))],
4428                window,
4429                cx,
4430            );
4431        });
4432
4433        // Append a message that contains a ResourceLink mention.
4434        message_editor.update_in(cx, |editor, window, cx| {
4435            editor.append_message(
4436                vec![acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
4437                    "file.txt",
4438                    "file:///project/file.txt",
4439                ))],
4440                Some("\n\n"),
4441                window,
4442                cx,
4443            );
4444        });
4445
4446        cx.run_until_parked();
4447
4448        // The mention should be registered in the mention_set so that contents()
4449        // will emit it as a structured block rather than plain text.
4450        let mention_uris =
4451            message_editor.update(cx, |editor, cx| editor.mention_set.read(cx).mentions());
4452        assert_eq!(
4453            mention_uris.len(),
4454            1,
4455            "Expected exactly one mention in the mention_set after append, got: {mention_uris:?}"
4456        );
4457
4458        // The editor text should start with the prefix, then the separator, then
4459        // the mention placeholder — confirming the offset was computed correctly.
4460        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4461        assert!(
4462            text.starts_with("prefix text\n\n"),
4463            "Expected text to start with 'prefix text\\n\\n', got: {text:?}"
4464        );
4465    }
4466}