message_editor.rs

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