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