message_editor.rs

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