message_editor.rs

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