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