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                    images.push(gpui::Image::from_bytes(format, content));
1370                }
1371
1372                crate::mention_set::insert_images_as_context(
1373                    images,
1374                    editor,
1375                    mention_set,
1376                    workspace,
1377                    cx,
1378                )
1379                .await;
1380                Ok(())
1381            })
1382            .detach_and_log_err(cx);
1383    }
1384
1385    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1386        self.editor.update(cx, |message_editor, cx| {
1387            message_editor.set_read_only(read_only);
1388            cx.notify()
1389        })
1390    }
1391
1392    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1393        self.editor.update(cx, |editor, cx| {
1394            if *editor.mode() != mode {
1395                editor.set_mode(mode);
1396                cx.notify()
1397            }
1398        });
1399    }
1400
1401    pub fn set_message(
1402        &mut self,
1403        message: Vec<acp::ContentBlock>,
1404        window: &mut Window,
1405        cx: &mut Context<Self>,
1406    ) {
1407        self.clear(window, cx);
1408        self.insert_message_blocks(message, false, window, cx);
1409    }
1410
1411    pub fn append_message(
1412        &mut self,
1413        message: Vec<acp::ContentBlock>,
1414        separator: Option<&str>,
1415        window: &mut Window,
1416        cx: &mut Context<Self>,
1417    ) {
1418        if message.is_empty() {
1419            return;
1420        }
1421
1422        if let Some(separator) = separator
1423            && !separator.is_empty()
1424            && !self.is_empty(cx)
1425        {
1426            self.editor.update(cx, |editor, cx| {
1427                editor.insert(separator, window, cx);
1428            });
1429        }
1430
1431        self.insert_message_blocks(message, true, window, cx);
1432    }
1433
1434    fn insert_message_blocks(
1435        &mut self,
1436        message: Vec<acp::ContentBlock>,
1437        append_to_existing: bool,
1438        window: &mut Window,
1439        cx: &mut Context<Self>,
1440    ) {
1441        let Some(workspace) = self.workspace.upgrade() else {
1442            return;
1443        };
1444
1445        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1446        let mut text = String::new();
1447        let mut mentions = Vec::new();
1448
1449        for chunk in message {
1450            match chunk {
1451                acp::ContentBlock::Text(text_content) => {
1452                    text.push_str(&text_content.text);
1453                }
1454                acp::ContentBlock::Resource(acp::EmbeddedResource {
1455                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1456                    ..
1457                }) => {
1458                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1459                    else {
1460                        continue;
1461                    };
1462                    let start = text.len();
1463                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1464                    let end = text.len();
1465                    mentions.push((
1466                        start..end,
1467                        mention_uri,
1468                        Mention::Text {
1469                            content: resource.text,
1470                            tracked_buffers: Vec::new(),
1471                        },
1472                    ));
1473                }
1474                acp::ContentBlock::ResourceLink(resource) => {
1475                    if let Some(mention_uri) =
1476                        MentionUri::parse(&resource.uri, path_style).log_err()
1477                    {
1478                        let start = text.len();
1479                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1480                        let end = text.len();
1481                        mentions.push((start..end, mention_uri, Mention::Link));
1482                    }
1483                }
1484                acp::ContentBlock::Image(acp::ImageContent {
1485                    uri,
1486                    data,
1487                    mime_type,
1488                    ..
1489                }) => {
1490                    let mention_uri = if let Some(uri) = uri {
1491                        MentionUri::parse(&uri, path_style)
1492                    } else {
1493                        Ok(MentionUri::PastedImage)
1494                    };
1495                    let Some(mention_uri) = mention_uri.log_err() else {
1496                        continue;
1497                    };
1498                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1499                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1500                        continue;
1501                    };
1502                    let start = text.len();
1503                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1504                    let end = text.len();
1505                    mentions.push((
1506                        start..end,
1507                        mention_uri,
1508                        Mention::Image(MentionImage {
1509                            data: data.into(),
1510                            format,
1511                        }),
1512                    ));
1513                }
1514                _ => {}
1515            }
1516        }
1517
1518        if text.is_empty() && mentions.is_empty() {
1519            return;
1520        }
1521
1522        let insertion_start = if append_to_existing {
1523            self.editor.read(cx).text(cx).len()
1524        } else {
1525            0
1526        };
1527
1528        let snapshot = if append_to_existing {
1529            self.editor.update(cx, |editor, cx| {
1530                editor.insert(&text, window, cx);
1531                editor.buffer().read(cx).snapshot(cx)
1532            })
1533        } else {
1534            self.editor.update(cx, |editor, cx| {
1535                editor.set_text(text, window, cx);
1536                editor.buffer().read(cx).snapshot(cx)
1537            })
1538        };
1539
1540        for (range, mention_uri, mention) in mentions {
1541            let adjusted_start = insertion_start + range.start;
1542            let anchor = snapshot.anchor_before(MultiBufferOffset(adjusted_start));
1543            let Some((crease_id, tx)) = insert_crease_for_mention(
1544                anchor.excerpt_id,
1545                anchor.text_anchor,
1546                range.end - range.start,
1547                mention_uri.name().into(),
1548                mention_uri.icon_path(cx),
1549                mention_uri.tooltip_text(),
1550                Some(mention_uri.clone()),
1551                Some(self.workspace.clone()),
1552                None,
1553                self.editor.clone(),
1554                window,
1555                cx,
1556            ) else {
1557                continue;
1558            };
1559            drop(tx);
1560
1561            self.mention_set.update(cx, |mention_set, _cx| {
1562                mention_set.insert_mention(
1563                    crease_id,
1564                    mention_uri.clone(),
1565                    Task::ready(Ok(mention)).shared(),
1566                )
1567            });
1568        }
1569
1570        cx.notify();
1571    }
1572
1573    pub fn text(&self, cx: &App) -> String {
1574        self.editor.read(cx).text(cx)
1575    }
1576
1577    pub fn insert_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1578        if text.is_empty() {
1579            return;
1580        }
1581
1582        self.editor.update(cx, |editor, cx| {
1583            editor.insert(text, window, cx);
1584        });
1585    }
1586
1587    pub fn set_placeholder_text(
1588        &mut self,
1589        placeholder: &str,
1590        window: &mut Window,
1591        cx: &mut Context<Self>,
1592    ) {
1593        self.editor.update(cx, |editor, cx| {
1594            editor.set_placeholder_text(placeholder, window, cx);
1595        });
1596    }
1597
1598    #[cfg(any(test, feature = "test-support"))]
1599    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1600        self.editor.update(cx, |editor, cx| {
1601            editor.set_text(text, window, cx);
1602        });
1603    }
1604}
1605
1606impl Focusable for MessageEditor {
1607    fn focus_handle(&self, cx: &App) -> FocusHandle {
1608        self.editor.focus_handle(cx)
1609    }
1610}
1611
1612impl Render for MessageEditor {
1613    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1614        div()
1615            .key_context("MessageEditor")
1616            .on_action(cx.listener(Self::chat))
1617            .on_action(cx.listener(Self::send_immediately))
1618            .on_action(cx.listener(Self::chat_with_follow))
1619            .on_action(cx.listener(Self::cancel))
1620            .on_action(cx.listener(Self::paste_raw))
1621            .capture_action(cx.listener(Self::paste))
1622            .flex_1()
1623            .child({
1624                let settings = ThemeSettings::get_global(cx);
1625
1626                let text_style = TextStyle {
1627                    color: cx.theme().colors().text,
1628                    font_family: settings.buffer_font.family.clone(),
1629                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1630                    font_features: settings.buffer_font.features.clone(),
1631                    font_size: settings.agent_buffer_font_size(cx).into(),
1632                    font_weight: settings.buffer_font.weight,
1633                    line_height: relative(settings.buffer_line_height.value()),
1634                    ..Default::default()
1635                };
1636
1637                EditorElement::new(
1638                    &self.editor,
1639                    EditorStyle {
1640                        background: cx.theme().colors().editor_background,
1641                        local_player: cx.theme().players().local(),
1642                        text: text_style,
1643                        syntax: cx.theme().syntax().clone(),
1644                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1645                        ..Default::default()
1646                    },
1647                )
1648            })
1649    }
1650}
1651
1652pub struct MessageEditorAddon {}
1653
1654impl MessageEditorAddon {
1655    pub fn new() -> Self {
1656        Self {}
1657    }
1658}
1659
1660impl Addon for MessageEditorAddon {
1661    fn to_any(&self) -> &dyn std::any::Any {
1662        self
1663    }
1664
1665    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1666        Some(self)
1667    }
1668
1669    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1670        let settings = agent_settings::AgentSettings::get_global(cx);
1671        if settings.use_modifier_to_send {
1672            key_context.add("use_modifier_to_send");
1673        }
1674    }
1675}
1676
1677/// Parses markdown mention links in the format `[@name](uri)` from text.
1678/// Returns a vector of (range, MentionUri) pairs where range is the byte range in the text.
1679fn parse_mention_links(text: &str, path_style: PathStyle) -> Vec<(Range<usize>, MentionUri)> {
1680    let mut mentions = Vec::new();
1681    let mut search_start = 0;
1682
1683    while let Some(link_start) = text[search_start..].find("[@") {
1684        let absolute_start = search_start + link_start;
1685
1686        // Find the matching closing bracket for the name, handling nested brackets.
1687        // Start at the '[' character so find_matching_bracket can track depth correctly.
1688        let Some(name_end) = find_matching_bracket(&text[absolute_start..], '[', ']') else {
1689            search_start = absolute_start + 2;
1690            continue;
1691        };
1692        let name_end = absolute_start + name_end;
1693
1694        // Check for opening parenthesis immediately after
1695        if text.get(name_end + 1..name_end + 2) != Some("(") {
1696            search_start = name_end + 1;
1697            continue;
1698        }
1699
1700        // Find the matching closing parenthesis for the URI, handling nested parens
1701        let uri_start = name_end + 2;
1702        let Some(uri_end_relative) = find_matching_bracket(&text[name_end + 1..], '(', ')') else {
1703            search_start = uri_start;
1704            continue;
1705        };
1706        let uri_end = name_end + 1 + uri_end_relative;
1707        let link_end = uri_end + 1;
1708
1709        let uri_str = &text[uri_start..uri_end];
1710
1711        // Try to parse the URI as a MentionUri
1712        if let Ok(mention_uri) = MentionUri::parse(uri_str, path_style) {
1713            mentions.push((absolute_start..link_end, mention_uri));
1714        }
1715
1716        search_start = link_end;
1717    }
1718
1719    mentions
1720}
1721
1722/// Finds the position of the matching closing bracket, handling nested brackets.
1723/// The input `text` should start with the opening bracket.
1724/// Returns the index of the matching closing bracket relative to `text`.
1725fn find_matching_bracket(text: &str, open: char, close: char) -> Option<usize> {
1726    let mut depth = 0;
1727    for (index, character) in text.char_indices() {
1728        if character == open {
1729            depth += 1;
1730        } else if character == close {
1731            depth -= 1;
1732            if depth == 0 {
1733                return Some(index);
1734            }
1735        }
1736    }
1737    None
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742    use std::{ops::Range, path::Path, sync::Arc};
1743
1744    use acp_thread::MentionUri;
1745    use agent::{ThreadStore, outline};
1746    use agent_client_protocol as acp;
1747    use editor::{
1748        AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset, SelectionEffects,
1749        actions::Paste,
1750    };
1751
1752    use fs::FakeFs;
1753    use futures::StreamExt as _;
1754    use gpui::{
1755        AppContext, ClipboardItem, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext,
1756        VisualTestContext,
1757    };
1758    use language_model::LanguageModelRegistry;
1759    use lsp::{CompletionContext, CompletionTriggerKind};
1760    use parking_lot::RwLock;
1761    use project::{CompletionIntent, Project, ProjectPath};
1762    use serde_json::json;
1763
1764    use text::Point;
1765    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1766    use util::{path, paths::PathStyle, rel_path::rel_path};
1767    use workspace::{AppState, Item, MultiWorkspace};
1768
1769    use crate::completion_provider::PromptContextType;
1770    use crate::{
1771        conversation_view::tests::init_test,
1772        message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
1773    };
1774
1775    #[test]
1776    fn test_parse_mention_links() {
1777        // Single file mention
1778        let text = "[@bundle-mac](file:///Users/test/zed/script/bundle-mac)";
1779        let mentions = parse_mention_links(text, PathStyle::local());
1780        assert_eq!(mentions.len(), 1);
1781        assert_eq!(mentions[0].0, 0..text.len());
1782        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
1783
1784        // Multiple mentions
1785        let text = "Check [@file1](file:///path/to/file1) and [@file2](file:///path/to/file2)!";
1786        let mentions = parse_mention_links(text, PathStyle::local());
1787        assert_eq!(mentions.len(), 2);
1788
1789        // Text without mentions
1790        let text = "Just some regular text without mentions";
1791        let mentions = parse_mention_links(text, PathStyle::local());
1792        assert_eq!(mentions.len(), 0);
1793
1794        // Malformed mentions (should be skipped)
1795        let text = "[@incomplete](invalid://uri) and [@missing](";
1796        let mentions = parse_mention_links(text, PathStyle::local());
1797        assert_eq!(mentions.len(), 0);
1798
1799        // Mixed content with valid mention
1800        let text = "Before [@valid](file:///path/to/file) after";
1801        let mentions = parse_mention_links(text, PathStyle::local());
1802        assert_eq!(mentions.len(), 1);
1803        assert_eq!(mentions[0].0.start, 7);
1804
1805        // HTTP URL mention (Fetch)
1806        let text = "Check out [@docs](https://example.com/docs) for more info";
1807        let mentions = parse_mention_links(text, PathStyle::local());
1808        assert_eq!(mentions.len(), 1);
1809        assert!(matches!(mentions[0].1, MentionUri::Fetch { .. }));
1810
1811        // Directory mention (trailing slash)
1812        let text = "[@src](file:///path/to/src/)";
1813        let mentions = parse_mention_links(text, PathStyle::local());
1814        assert_eq!(mentions.len(), 1);
1815        assert!(matches!(mentions[0].1, MentionUri::Directory { .. }));
1816
1817        // Multiple different mention types
1818        let text = "File [@f](file:///a) and URL [@u](https://b.com) and dir [@d](file:///c/)";
1819        let mentions = parse_mention_links(text, PathStyle::local());
1820        assert_eq!(mentions.len(), 3);
1821        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
1822        assert!(matches!(mentions[1].1, MentionUri::Fetch { .. }));
1823        assert!(matches!(mentions[2].1, MentionUri::Directory { .. }));
1824
1825        // Adjacent mentions without separator
1826        let text = "[@a](file:///a)[@b](file:///b)";
1827        let mentions = parse_mention_links(text, PathStyle::local());
1828        assert_eq!(mentions.len(), 2);
1829
1830        // Regular markdown link (not a mention) should be ignored
1831        let text = "[regular link](https://example.com)";
1832        let mentions = parse_mention_links(text, PathStyle::local());
1833        assert_eq!(mentions.len(), 0);
1834
1835        // Incomplete mention link patterns
1836        let text = "[@name] without url and [@name( malformed";
1837        let mentions = parse_mention_links(text, PathStyle::local());
1838        assert_eq!(mentions.len(), 0);
1839
1840        // Nested brackets in name portion
1841        let text = "[@name [with brackets]](file:///path/to/file)";
1842        let mentions = parse_mention_links(text, PathStyle::local());
1843        assert_eq!(mentions.len(), 1);
1844        assert_eq!(mentions[0].0, 0..text.len());
1845
1846        // Deeply nested brackets
1847        let text = "[@outer [inner [deep]]](file:///path)";
1848        let mentions = parse_mention_links(text, PathStyle::local());
1849        assert_eq!(mentions.len(), 1);
1850
1851        // Unbalanced brackets should fail gracefully
1852        let text = "[@unbalanced [bracket](file:///path)";
1853        let mentions = parse_mention_links(text, PathStyle::local());
1854        assert_eq!(mentions.len(), 0);
1855
1856        // Nested parentheses in URI (common in URLs with query params)
1857        let text = "[@wiki](https://en.wikipedia.org/wiki/Rust_(programming_language))";
1858        let mentions = parse_mention_links(text, PathStyle::local());
1859        assert_eq!(mentions.len(), 1);
1860        if let MentionUri::Fetch { url } = &mentions[0].1 {
1861            assert!(url.as_str().contains("Rust_(programming_language)"));
1862        } else {
1863            panic!("Expected Fetch URI");
1864        }
1865    }
1866
1867    #[gpui::test]
1868    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1869        init_test(cx);
1870
1871        let fs = FakeFs::new(cx.executor());
1872        fs.insert_tree("/project", json!({"file": ""})).await;
1873        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1874
1875        let (multi_workspace, cx) =
1876            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1877        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1878
1879        let thread_store = None;
1880
1881        let message_editor = cx.update(|window, cx| {
1882            cx.new(|cx| {
1883                MessageEditor::new(
1884                    workspace.downgrade(),
1885                    project.downgrade(),
1886                    thread_store.clone(),
1887                    None,
1888                    None,
1889                    Default::default(),
1890                    "Test Agent".into(),
1891                    "Test",
1892                    EditorMode::AutoHeight {
1893                        min_lines: 1,
1894                        max_lines: None,
1895                    },
1896                    window,
1897                    cx,
1898                )
1899            })
1900        });
1901        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1902
1903        cx.run_until_parked();
1904
1905        let excerpt_id = editor.update(cx, |editor, cx| {
1906            editor
1907                .buffer()
1908                .read(cx)
1909                .excerpt_ids()
1910                .into_iter()
1911                .next()
1912                .unwrap()
1913        });
1914        let completions = editor.update_in(cx, |editor, window, cx| {
1915            editor.set_text("Hello @file ", window, cx);
1916            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1917            let completion_provider = editor.completion_provider().unwrap();
1918            completion_provider.completions(
1919                excerpt_id,
1920                &buffer,
1921                text::Anchor::MAX,
1922                CompletionContext {
1923                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1924                    trigger_character: Some("@".into()),
1925                },
1926                window,
1927                cx,
1928            )
1929        });
1930        let [_, completion]: [_; 2] = completions
1931            .await
1932            .unwrap()
1933            .into_iter()
1934            .flat_map(|response| response.completions)
1935            .collect::<Vec<_>>()
1936            .try_into()
1937            .unwrap();
1938
1939        editor.update_in(cx, |editor, window, cx| {
1940            let snapshot = editor.buffer().read(cx).snapshot(cx);
1941            let range = snapshot
1942                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1943                .unwrap();
1944            editor.edit([(range, completion.new_text)], cx);
1945            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1946        });
1947
1948        cx.run_until_parked();
1949
1950        // Backspace over the inserted crease (and the following space).
1951        editor.update_in(cx, |editor, window, cx| {
1952            editor.backspace(&Default::default(), window, cx);
1953            editor.backspace(&Default::default(), window, cx);
1954        });
1955
1956        let (content, _) = message_editor
1957            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1958            .await
1959            .unwrap();
1960
1961        // We don't send a resource link for the deleted crease.
1962        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1963    }
1964
1965    #[gpui::test]
1966    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1967        init_test(cx);
1968        let fs = FakeFs::new(cx.executor());
1969        fs.insert_tree(
1970            "/test",
1971            json!({
1972                ".zed": {
1973                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1974                },
1975                "src": {
1976                    "main.rs": "fn main() {}",
1977                },
1978            }),
1979        )
1980        .await;
1981
1982        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1983        let thread_store = None;
1984        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
1985            acp::PromptCapabilities::default(),
1986            vec![],
1987        )));
1988
1989        let (multi_workspace, cx) =
1990            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1991        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1992        let workspace_handle = workspace.downgrade();
1993        let message_editor = workspace.update_in(cx, |_, window, cx| {
1994            cx.new(|cx| {
1995                MessageEditor::new(
1996                    workspace_handle.clone(),
1997                    project.downgrade(),
1998                    thread_store.clone(),
1999                    None,
2000                    None,
2001                    session_capabilities.clone(),
2002                    "Claude Agent".into(),
2003                    "Test",
2004                    EditorMode::AutoHeight {
2005                        min_lines: 1,
2006                        max_lines: None,
2007                    },
2008                    window,
2009                    cx,
2010                )
2011            })
2012        });
2013        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2014
2015        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
2016        editor.update_in(cx, |editor, window, cx| {
2017            editor.set_text("/file test.txt", window, cx);
2018        });
2019
2020        let contents_result = message_editor
2021            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2022            .await;
2023
2024        // Should fail because available_commands is empty (no commands supported)
2025        assert!(contents_result.is_err());
2026        let error_message = contents_result.unwrap_err().to_string();
2027        assert!(error_message.contains("not supported by Claude Agent"));
2028        assert!(error_message.contains("Available commands: none"));
2029
2030        // Now simulate Claude providing its list of available commands (which doesn't include file)
2031        session_capabilities
2032            .write()
2033            .set_available_commands(vec![acp::AvailableCommand::new("help", "Get help")]);
2034
2035        // Test that unsupported slash commands trigger an error when we have a list of available commands
2036        editor.update_in(cx, |editor, window, cx| {
2037            editor.set_text("/file test.txt", window, cx);
2038        });
2039
2040        let contents_result = message_editor
2041            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2042            .await;
2043
2044        assert!(contents_result.is_err());
2045        let error_message = contents_result.unwrap_err().to_string();
2046        assert!(error_message.contains("not supported by Claude Agent"));
2047        assert!(error_message.contains("/file"));
2048        assert!(error_message.contains("Available commands: /help"));
2049
2050        // Test that supported commands work fine
2051        editor.update_in(cx, |editor, window, cx| {
2052            editor.set_text("/help", window, cx);
2053        });
2054
2055        let contents_result = message_editor
2056            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2057            .await;
2058
2059        // Should succeed because /help is in available_commands
2060        assert!(contents_result.is_ok());
2061
2062        // Test that regular text works fine
2063        editor.update_in(cx, |editor, window, cx| {
2064            editor.set_text("Hello Claude!", window, cx);
2065        });
2066
2067        let (content, _) = message_editor
2068            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2069            .await
2070            .unwrap();
2071
2072        assert_eq!(content.len(), 1);
2073        if let acp::ContentBlock::Text(text) = &content[0] {
2074            assert_eq!(text.text, "Hello Claude!");
2075        } else {
2076            panic!("Expected ContentBlock::Text");
2077        }
2078
2079        // Test that @ mentions still work
2080        editor.update_in(cx, |editor, window, cx| {
2081            editor.set_text("Check this @", window, cx);
2082        });
2083
2084        // The @ mention functionality should not be affected
2085        let (content, _) = message_editor
2086            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2087            .await
2088            .unwrap();
2089
2090        assert_eq!(content.len(), 1);
2091        if let acp::ContentBlock::Text(text) = &content[0] {
2092            assert_eq!(text.text, "Check this @");
2093        } else {
2094            panic!("Expected ContentBlock::Text");
2095        }
2096    }
2097
2098    struct MessageEditorItem(Entity<MessageEditor>);
2099
2100    impl Item for MessageEditorItem {
2101        type Event = ();
2102
2103        fn include_in_nav_history() -> bool {
2104            false
2105        }
2106
2107        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
2108            "Test".into()
2109        }
2110    }
2111
2112    impl EventEmitter<()> for MessageEditorItem {}
2113
2114    impl Focusable for MessageEditorItem {
2115        fn focus_handle(&self, cx: &App) -> FocusHandle {
2116            self.0.read(cx).focus_handle(cx)
2117        }
2118    }
2119
2120    impl Render for MessageEditorItem {
2121        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2122            self.0.clone().into_any_element()
2123        }
2124    }
2125
2126    #[gpui::test]
2127    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
2128        init_test(cx);
2129
2130        let app_state = cx.update(AppState::test);
2131
2132        cx.update(|cx| {
2133            editor::init(cx);
2134            workspace::init(app_state.clone(), cx);
2135        });
2136
2137        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2138        let window =
2139            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2140        let workspace = window
2141            .read_with(cx, |mw, _| mw.workspace().clone())
2142            .unwrap();
2143
2144        let mut cx = VisualTestContext::from_window(window.into(), cx);
2145
2146        let thread_store = None;
2147        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2148            acp::PromptCapabilities::default(),
2149            vec![
2150                acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
2151                acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
2152                    acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
2153                        "<name>",
2154                    )),
2155                ),
2156            ],
2157        )));
2158
2159        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2160            let workspace_handle = cx.weak_entity();
2161            let message_editor = cx.new(|cx| {
2162                MessageEditor::new(
2163                    workspace_handle,
2164                    project.downgrade(),
2165                    thread_store.clone(),
2166                    None,
2167                    None,
2168                    session_capabilities.clone(),
2169                    "Test Agent".into(),
2170                    "Test",
2171                    EditorMode::AutoHeight {
2172                        max_lines: None,
2173                        min_lines: 1,
2174                    },
2175                    window,
2176                    cx,
2177                )
2178            });
2179            workspace.active_pane().update(cx, |pane, cx| {
2180                pane.add_item(
2181                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2182                    true,
2183                    true,
2184                    None,
2185                    window,
2186                    cx,
2187                );
2188            });
2189            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2190            message_editor.read(cx).editor().clone()
2191        });
2192
2193        cx.simulate_input("/");
2194
2195        editor.update_in(&mut cx, |editor, window, cx| {
2196            assert_eq!(editor.text(cx), "/");
2197            assert!(editor.has_visible_completions_menu());
2198
2199            assert_eq!(
2200                current_completion_labels_with_documentation(editor),
2201                &[
2202                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
2203                    ("say-hello".into(), "Say hello to whoever you want".into())
2204                ]
2205            );
2206            editor.set_text("", window, cx);
2207        });
2208
2209        cx.simulate_input("/qui");
2210
2211        editor.update_in(&mut cx, |editor, window, cx| {
2212            assert_eq!(editor.text(cx), "/qui");
2213            assert!(editor.has_visible_completions_menu());
2214
2215            assert_eq!(
2216                current_completion_labels_with_documentation(editor),
2217                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
2218            );
2219            editor.set_text("", window, cx);
2220        });
2221
2222        editor.update_in(&mut cx, |editor, window, cx| {
2223            assert!(editor.has_visible_completions_menu());
2224            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2225        });
2226
2227        cx.run_until_parked();
2228
2229        editor.update_in(&mut cx, |editor, window, cx| {
2230            assert_eq!(editor.display_text(cx), "/quick-math ");
2231            assert!(!editor.has_visible_completions_menu());
2232            editor.set_text("", window, cx);
2233        });
2234
2235        cx.simulate_input("/say");
2236
2237        editor.update_in(&mut cx, |editor, _window, cx| {
2238            assert_eq!(editor.display_text(cx), "/say");
2239            assert!(editor.has_visible_completions_menu());
2240
2241            assert_eq!(
2242                current_completion_labels_with_documentation(editor),
2243                &[("say-hello".into(), "Say hello to whoever you want".into())]
2244            );
2245        });
2246
2247        editor.update_in(&mut cx, |editor, window, cx| {
2248            assert!(editor.has_visible_completions_menu());
2249            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2250        });
2251
2252        cx.run_until_parked();
2253
2254        editor.update_in(&mut cx, |editor, _window, cx| {
2255            assert_eq!(editor.text(cx), "/say-hello ");
2256            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2257            assert!(!editor.has_visible_completions_menu());
2258        });
2259
2260        cx.simulate_input("GPT5");
2261
2262        cx.run_until_parked();
2263
2264        editor.update_in(&mut cx, |editor, window, cx| {
2265            assert_eq!(editor.text(cx), "/say-hello GPT5");
2266            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2267            assert!(!editor.has_visible_completions_menu());
2268
2269            // Delete argument
2270            for _ in 0..5 {
2271                editor.backspace(&editor::actions::Backspace, window, cx);
2272            }
2273        });
2274
2275        cx.run_until_parked();
2276
2277        editor.update_in(&mut cx, |editor, window, cx| {
2278            assert_eq!(editor.text(cx), "/say-hello");
2279            // Hint is visible because argument was deleted
2280            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2281
2282            // Delete last command letter
2283            editor.backspace(&editor::actions::Backspace, window, cx);
2284        });
2285
2286        cx.run_until_parked();
2287
2288        editor.update_in(&mut cx, |editor, _window, cx| {
2289            // Hint goes away once command no longer matches an available one
2290            assert_eq!(editor.text(cx), "/say-hell");
2291            assert_eq!(editor.display_text(cx), "/say-hell");
2292            assert!(!editor.has_visible_completions_menu());
2293        });
2294    }
2295
2296    #[gpui::test]
2297    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2298        init_test(cx);
2299
2300        let app_state = cx.update(AppState::test);
2301
2302        cx.update(|cx| {
2303            editor::init(cx);
2304            workspace::init(app_state.clone(), cx);
2305        });
2306
2307        app_state
2308            .fs
2309            .as_fake()
2310            .insert_tree(
2311                path!("/dir"),
2312                json!({
2313                    "editor": "",
2314                    "a": {
2315                        "one.txt": "1",
2316                        "two.txt": "2",
2317                        "three.txt": "3",
2318                        "four.txt": "4"
2319                    },
2320                    "b": {
2321                        "five.txt": "5",
2322                        "six.txt": "6",
2323                        "seven.txt": "7",
2324                        "eight.txt": "8",
2325                    },
2326                    "x.png": "",
2327                }),
2328            )
2329            .await;
2330
2331        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2332        let window =
2333            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2334        let workspace = window
2335            .read_with(cx, |mw, _| mw.workspace().clone())
2336            .unwrap();
2337
2338        let worktree = project.update(cx, |project, cx| {
2339            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2340            assert_eq!(worktrees.len(), 1);
2341            worktrees.pop().unwrap()
2342        });
2343        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2344
2345        let mut cx = VisualTestContext::from_window(window.into(), cx);
2346
2347        let paths = vec![
2348            rel_path("a/one.txt"),
2349            rel_path("a/two.txt"),
2350            rel_path("a/three.txt"),
2351            rel_path("a/four.txt"),
2352            rel_path("b/five.txt"),
2353            rel_path("b/six.txt"),
2354            rel_path("b/seven.txt"),
2355            rel_path("b/eight.txt"),
2356        ];
2357
2358        let slash = PathStyle::local().primary_separator();
2359
2360        let mut opened_editors = Vec::new();
2361        for path in paths {
2362            let buffer = workspace
2363                .update_in(&mut cx, |workspace, window, cx| {
2364                    workspace.open_path(
2365                        ProjectPath {
2366                            worktree_id,
2367                            path: path.into(),
2368                        },
2369                        None,
2370                        false,
2371                        window,
2372                        cx,
2373                    )
2374                })
2375                .await
2376                .unwrap();
2377            opened_editors.push(buffer);
2378        }
2379
2380        let thread_store = cx.new(|cx| ThreadStore::new(cx));
2381        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
2382            acp::PromptCapabilities::default(),
2383            vec![],
2384        )));
2385
2386        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2387            let workspace_handle = cx.weak_entity();
2388            let message_editor = cx.new(|cx| {
2389                MessageEditor::new(
2390                    workspace_handle,
2391                    project.downgrade(),
2392                    Some(thread_store),
2393                    None,
2394                    None,
2395                    session_capabilities.clone(),
2396                    "Test Agent".into(),
2397                    "Test",
2398                    EditorMode::AutoHeight {
2399                        max_lines: None,
2400                        min_lines: 1,
2401                    },
2402                    window,
2403                    cx,
2404                )
2405            });
2406            workspace.active_pane().update(cx, |pane, cx| {
2407                pane.add_item(
2408                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2409                    true,
2410                    true,
2411                    None,
2412                    window,
2413                    cx,
2414                );
2415            });
2416            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2417            let editor = message_editor.read(cx).editor().clone();
2418            (message_editor, editor)
2419        });
2420
2421        cx.simulate_input("Lorem @");
2422
2423        editor.update_in(&mut cx, |editor, window, cx| {
2424            assert_eq!(editor.text(cx), "Lorem @");
2425            assert!(editor.has_visible_completions_menu());
2426
2427            assert_eq!(
2428                current_completion_labels(editor),
2429                &[
2430                    format!("eight.txt b{slash}"),
2431                    format!("seven.txt b{slash}"),
2432                    format!("six.txt b{slash}"),
2433                    format!("five.txt b{slash}"),
2434                    "Files & Directories".into(),
2435                    "Symbols".into()
2436                ]
2437            );
2438            editor.set_text("", window, cx);
2439        });
2440
2441        message_editor.update(&mut cx, |editor, _cx| {
2442            editor.session_capabilities.write().set_prompt_capabilities(
2443                acp::PromptCapabilities::new()
2444                    .image(true)
2445                    .audio(true)
2446                    .embedded_context(true),
2447            );
2448        });
2449
2450        cx.simulate_input("Lorem ");
2451
2452        editor.update(&mut cx, |editor, cx| {
2453            assert_eq!(editor.text(cx), "Lorem ");
2454            assert!(!editor.has_visible_completions_menu());
2455        });
2456
2457        cx.simulate_input("@");
2458
2459        editor.update(&mut cx, |editor, cx| {
2460            assert_eq!(editor.text(cx), "Lorem @");
2461            assert!(editor.has_visible_completions_menu());
2462            assert_eq!(
2463                current_completion_labels(editor),
2464                &[
2465                    format!("eight.txt b{slash}"),
2466                    format!("seven.txt b{slash}"),
2467                    format!("six.txt b{slash}"),
2468                    format!("five.txt b{slash}"),
2469                    "Files & Directories".into(),
2470                    "Symbols".into(),
2471                    "Threads".into(),
2472                    "Fetch".into()
2473                ]
2474            );
2475        });
2476
2477        // Select and confirm "File"
2478        editor.update_in(&mut cx, |editor, window, cx| {
2479            assert!(editor.has_visible_completions_menu());
2480            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2481            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2482            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2483            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2484            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2485        });
2486
2487        cx.run_until_parked();
2488
2489        editor.update(&mut cx, |editor, cx| {
2490            assert_eq!(editor.text(cx), "Lorem @file ");
2491            assert!(editor.has_visible_completions_menu());
2492        });
2493
2494        cx.simulate_input("one");
2495
2496        editor.update(&mut cx, |editor, cx| {
2497            assert_eq!(editor.text(cx), "Lorem @file one");
2498            assert!(editor.has_visible_completions_menu());
2499            assert_eq!(
2500                current_completion_labels(editor),
2501                vec![format!("one.txt a{slash}")]
2502            );
2503        });
2504
2505        editor.update_in(&mut cx, |editor, window, cx| {
2506            assert!(editor.has_visible_completions_menu());
2507            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2508        });
2509
2510        let url_one = MentionUri::File {
2511            abs_path: path!("/dir/a/one.txt").into(),
2512        }
2513        .to_uri()
2514        .to_string();
2515        editor.update(&mut cx, |editor, cx| {
2516            let text = editor.text(cx);
2517            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2518            assert!(!editor.has_visible_completions_menu());
2519            assert_eq!(fold_ranges(editor, cx).len(), 1);
2520        });
2521
2522        let contents = message_editor
2523            .update(&mut cx, |message_editor, cx| {
2524                message_editor
2525                    .mention_set()
2526                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2527            })
2528            .await
2529            .unwrap()
2530            .into_values()
2531            .collect::<Vec<_>>();
2532
2533        {
2534            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2535                panic!("Unexpected mentions");
2536            };
2537            pretty_assertions::assert_eq!(content, "1");
2538            pretty_assertions::assert_eq!(
2539                uri,
2540                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2541            );
2542        }
2543
2544        cx.simulate_input(" ");
2545
2546        editor.update(&mut cx, |editor, cx| {
2547            let text = editor.text(cx);
2548            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2549            assert!(!editor.has_visible_completions_menu());
2550            assert_eq!(fold_ranges(editor, cx).len(), 1);
2551        });
2552
2553        cx.simulate_input("Ipsum ");
2554
2555        editor.update(&mut cx, |editor, cx| {
2556            let text = editor.text(cx);
2557            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2558            assert!(!editor.has_visible_completions_menu());
2559            assert_eq!(fold_ranges(editor, cx).len(), 1);
2560        });
2561
2562        cx.simulate_input("@file ");
2563
2564        editor.update(&mut cx, |editor, cx| {
2565            let text = editor.text(cx);
2566            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2567            assert!(editor.has_visible_completions_menu());
2568            assert_eq!(fold_ranges(editor, cx).len(), 1);
2569        });
2570
2571        editor.update_in(&mut cx, |editor, window, cx| {
2572            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2573        });
2574
2575        cx.run_until_parked();
2576
2577        let contents = message_editor
2578            .update(&mut cx, |message_editor, cx| {
2579                message_editor
2580                    .mention_set()
2581                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2582            })
2583            .await
2584            .unwrap()
2585            .into_values()
2586            .collect::<Vec<_>>();
2587
2588        let url_eight = MentionUri::File {
2589            abs_path: path!("/dir/b/eight.txt").into(),
2590        }
2591        .to_uri()
2592        .to_string();
2593
2594        {
2595            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2596                panic!("Unexpected mentions");
2597            };
2598            pretty_assertions::assert_eq!(content, "8");
2599            pretty_assertions::assert_eq!(
2600                uri,
2601                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2602            );
2603        }
2604
2605        editor.update(&mut cx, |editor, cx| {
2606            assert_eq!(
2607                editor.text(cx),
2608                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2609            );
2610            assert!(!editor.has_visible_completions_menu());
2611            assert_eq!(fold_ranges(editor, cx).len(), 2);
2612        });
2613
2614        let plain_text_language = Arc::new(language::Language::new(
2615            language::LanguageConfig {
2616                name: "Plain Text".into(),
2617                matcher: language::LanguageMatcher {
2618                    path_suffixes: vec!["txt".to_string()],
2619                    ..Default::default()
2620                },
2621                ..Default::default()
2622            },
2623            None,
2624        ));
2625
2626        // Register the language and fake LSP
2627        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2628        language_registry.add(plain_text_language);
2629
2630        let mut fake_language_servers = language_registry.register_fake_lsp(
2631            "Plain Text",
2632            language::FakeLspAdapter {
2633                capabilities: lsp::ServerCapabilities {
2634                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2635                    ..Default::default()
2636                },
2637                ..Default::default()
2638            },
2639        );
2640
2641        // Open the buffer to trigger LSP initialization
2642        let buffer = project
2643            .update(&mut cx, |project, cx| {
2644                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2645            })
2646            .await
2647            .unwrap();
2648
2649        // Register the buffer with language servers
2650        let _handle = project.update(&mut cx, |project, cx| {
2651            project.register_buffer_with_language_servers(&buffer, cx)
2652        });
2653
2654        cx.run_until_parked();
2655
2656        let fake_language_server = fake_language_servers.next().await.unwrap();
2657        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2658            move |_, _| async move {
2659                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2660                    #[allow(deprecated)]
2661                    lsp::SymbolInformation {
2662                        name: "MySymbol".into(),
2663                        location: lsp::Location {
2664                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2665                            range: lsp::Range::new(
2666                                lsp::Position::new(0, 0),
2667                                lsp::Position::new(0, 1),
2668                            ),
2669                        },
2670                        kind: lsp::SymbolKind::CONSTANT,
2671                        tags: None,
2672                        container_name: None,
2673                        deprecated: None,
2674                    },
2675                ])))
2676            },
2677        );
2678
2679        cx.simulate_input("@symbol ");
2680
2681        editor.update(&mut cx, |editor, cx| {
2682            assert_eq!(
2683                editor.text(cx),
2684                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2685            );
2686            assert!(editor.has_visible_completions_menu());
2687            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2688        });
2689
2690        editor.update_in(&mut cx, |editor, window, cx| {
2691            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2692        });
2693
2694        let symbol = MentionUri::Symbol {
2695            abs_path: path!("/dir/a/one.txt").into(),
2696            name: "MySymbol".into(),
2697            line_range: 0..=0,
2698        };
2699
2700        let contents = message_editor
2701            .update(&mut cx, |message_editor, cx| {
2702                message_editor
2703                    .mention_set()
2704                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2705            })
2706            .await
2707            .unwrap()
2708            .into_values()
2709            .collect::<Vec<_>>();
2710
2711        {
2712            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2713                panic!("Unexpected mentions");
2714            };
2715            pretty_assertions::assert_eq!(content, "1");
2716            pretty_assertions::assert_eq!(uri, &symbol);
2717        }
2718
2719        cx.run_until_parked();
2720
2721        editor.read_with(&cx, |editor, cx| {
2722            assert_eq!(
2723                editor.text(cx),
2724                format!(
2725                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2726                    symbol.to_uri(),
2727                )
2728            );
2729        });
2730
2731        // Try to mention an "image" file that will fail to load
2732        cx.simulate_input("@file x.png");
2733
2734        editor.update(&mut cx, |editor, cx| {
2735            assert_eq!(
2736                editor.text(cx),
2737                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2738            );
2739            assert!(editor.has_visible_completions_menu());
2740            assert_eq!(current_completion_labels(editor), &["x.png "]);
2741        });
2742
2743        editor.update_in(&mut cx, |editor, window, cx| {
2744            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2745        });
2746
2747        // Getting the message contents fails
2748        message_editor
2749            .update(&mut cx, |message_editor, cx| {
2750                message_editor
2751                    .mention_set()
2752                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2753            })
2754            .await
2755            .expect_err("Should fail to load x.png");
2756
2757        cx.run_until_parked();
2758
2759        // Mention was removed
2760        editor.read_with(&cx, |editor, cx| {
2761            assert_eq!(
2762                editor.text(cx),
2763                format!(
2764                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2765                    symbol.to_uri()
2766                )
2767            );
2768        });
2769
2770        // Once more
2771        cx.simulate_input("@file x.png");
2772
2773        editor.update(&mut cx, |editor, cx| {
2774                    assert_eq!(
2775                        editor.text(cx),
2776                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2777                    );
2778                    assert!(editor.has_visible_completions_menu());
2779                    assert_eq!(current_completion_labels(editor), &["x.png "]);
2780                });
2781
2782        editor.update_in(&mut cx, |editor, window, cx| {
2783            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2784        });
2785
2786        // This time don't immediately get the contents, just let the confirmed completion settle
2787        cx.run_until_parked();
2788
2789        // Mention was removed
2790        editor.read_with(&cx, |editor, cx| {
2791            assert_eq!(
2792                editor.text(cx),
2793                format!(
2794                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2795                    symbol.to_uri()
2796                )
2797            );
2798        });
2799
2800        // Now getting the contents succeeds, because the invalid mention was removed
2801        let contents = message_editor
2802            .update(&mut cx, |message_editor, cx| {
2803                message_editor
2804                    .mention_set()
2805                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2806            })
2807            .await
2808            .unwrap();
2809        assert_eq!(contents.len(), 3);
2810    }
2811
2812    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2813        let snapshot = editor.buffer().read(cx).snapshot(cx);
2814        editor.display_map.update(cx, |display_map, cx| {
2815            display_map
2816                .snapshot(cx)
2817                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
2818                .map(|fold| fold.range.to_point(&snapshot))
2819                .collect()
2820        })
2821    }
2822
2823    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2824        let completions = editor.current_completions().expect("Missing completions");
2825        completions
2826            .into_iter()
2827            .map(|completion| completion.label.text)
2828            .collect::<Vec<_>>()
2829    }
2830
2831    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2832        let completions = editor.current_completions().expect("Missing completions");
2833        completions
2834            .into_iter()
2835            .map(|completion| {
2836                (
2837                    completion.label.text,
2838                    completion
2839                        .documentation
2840                        .map(|d| d.text().to_string())
2841                        .unwrap_or_default(),
2842                )
2843            })
2844            .collect::<Vec<_>>()
2845    }
2846
2847    #[gpui::test]
2848    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
2849        init_test(cx);
2850
2851        let fs = FakeFs::new(cx.executor());
2852
2853        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2854        // Using plain text without a configured language, so no outline is available
2855        const LINE: &str = "This is a line of text in the file\n";
2856        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2857        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2858
2859        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2860        let small_content = "fn small_function() { /* small */ }\n";
2861        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2862
2863        fs.insert_tree(
2864            "/project",
2865            json!({
2866                "large_file.txt": large_content.clone(),
2867                "small_file.txt": small_content,
2868            }),
2869        )
2870        .await;
2871
2872        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2873
2874        let (multi_workspace, cx) =
2875            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2876        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2877
2878        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2879
2880        let message_editor = cx.update(|window, cx| {
2881            cx.new(|cx| {
2882                let editor = MessageEditor::new(
2883                    workspace.downgrade(),
2884                    project.downgrade(),
2885                    thread_store.clone(),
2886                    None,
2887                    None,
2888                    Default::default(),
2889                    "Test Agent".into(),
2890                    "Test",
2891                    EditorMode::AutoHeight {
2892                        min_lines: 1,
2893                        max_lines: None,
2894                    },
2895                    window,
2896                    cx,
2897                );
2898                // Enable embedded context so files are actually included
2899                editor
2900                    .session_capabilities
2901                    .write()
2902                    .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
2903                editor
2904            })
2905        });
2906
2907        // Test large file mention
2908        // Get the absolute path using the project's worktree
2909        let large_file_abs_path = project.read_with(cx, |project, cx| {
2910            let worktree = project.worktrees(cx).next().unwrap();
2911            let worktree_root = worktree.read(cx).abs_path();
2912            worktree_root.join("large_file.txt")
2913        });
2914        let large_file_task = message_editor.update(cx, |editor, cx| {
2915            editor.mention_set().update(cx, |set, cx| {
2916                set.confirm_mention_for_file(large_file_abs_path, true, cx)
2917            })
2918        });
2919
2920        let large_file_mention = large_file_task.await.unwrap();
2921        match large_file_mention {
2922            Mention::Text { content, .. } => {
2923                // Should contain some of the content but not all of it
2924                assert!(
2925                    content.contains(LINE),
2926                    "Should contain some of the file content"
2927                );
2928                assert!(
2929                    !content.contains(&LINE.repeat(100)),
2930                    "Should not contain the full file"
2931                );
2932                // Should be much smaller than original
2933                assert!(
2934                    content.len() < large_content.len() / 10,
2935                    "Should be significantly truncated"
2936                );
2937            }
2938            _ => panic!("Expected Text mention for large file"),
2939        }
2940
2941        // Test small file mention
2942        // Get the absolute path using the project's worktree
2943        let small_file_abs_path = project.read_with(cx, |project, cx| {
2944            let worktree = project.worktrees(cx).next().unwrap();
2945            let worktree_root = worktree.read(cx).abs_path();
2946            worktree_root.join("small_file.txt")
2947        });
2948        let small_file_task = message_editor.update(cx, |editor, cx| {
2949            editor.mention_set().update(cx, |set, cx| {
2950                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2951            })
2952        });
2953
2954        let small_file_mention = small_file_task.await.unwrap();
2955        match small_file_mention {
2956            Mention::Text { content, .. } => {
2957                // Should contain the full actual content
2958                assert_eq!(content, small_content);
2959            }
2960            _ => panic!("Expected Text mention for small file"),
2961        }
2962    }
2963
2964    #[gpui::test]
2965    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2966        init_test(cx);
2967        cx.update(LanguageModelRegistry::test);
2968
2969        let fs = FakeFs::new(cx.executor());
2970        fs.insert_tree("/project", json!({"file": ""})).await;
2971        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2972
2973        let (multi_workspace, cx) =
2974            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2975        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2976
2977        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2978
2979        let session_id = acp::SessionId::new("thread-123");
2980        let title = Some("Previous Conversation".into());
2981
2982        let message_editor = cx.update(|window, cx| {
2983            cx.new(|cx| {
2984                let mut editor = MessageEditor::new(
2985                    workspace.downgrade(),
2986                    project.downgrade(),
2987                    thread_store.clone(),
2988                    None,
2989                    None,
2990                    Default::default(),
2991                    "Test Agent".into(),
2992                    "Test",
2993                    EditorMode::AutoHeight {
2994                        min_lines: 1,
2995                        max_lines: None,
2996                    },
2997                    window,
2998                    cx,
2999                );
3000                editor.insert_thread_summary(session_id.clone(), title.clone(), window, cx);
3001                editor
3002            })
3003        });
3004
3005        // Construct expected values for verification
3006        let expected_uri = MentionUri::Thread {
3007            id: session_id.clone(),
3008            name: title.as_ref().unwrap().to_string(),
3009        };
3010        let expected_title = title.as_ref().unwrap();
3011        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
3012
3013        message_editor.read_with(cx, |editor, cx| {
3014            let text = editor.text(cx);
3015
3016            assert!(
3017                text.contains(&expected_link),
3018                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
3019                expected_link,
3020                text
3021            );
3022
3023            let mentions = editor.mention_set().read(cx).mentions();
3024            assert_eq!(
3025                mentions.len(),
3026                1,
3027                "Expected exactly one mention after inserting thread summary"
3028            );
3029
3030            assert!(
3031                mentions.contains(&expected_uri),
3032                "Expected mentions to contain the thread URI"
3033            );
3034        });
3035    }
3036
3037    #[gpui::test]
3038    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
3039        init_test(cx);
3040        cx.update(LanguageModelRegistry::test);
3041
3042        let fs = FakeFs::new(cx.executor());
3043        fs.insert_tree("/project", json!({"file": ""})).await;
3044        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3045
3046        let (multi_workspace, cx) =
3047            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3048        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3049
3050        let thread_store = None;
3051
3052        let message_editor = cx.update(|window, cx| {
3053            cx.new(|cx| {
3054                let mut editor = MessageEditor::new(
3055                    workspace.downgrade(),
3056                    project.downgrade(),
3057                    thread_store.clone(),
3058                    None,
3059                    None,
3060                    Default::default(),
3061                    "Test Agent".into(),
3062                    "Test",
3063                    EditorMode::AutoHeight {
3064                        min_lines: 1,
3065                        max_lines: None,
3066                    },
3067                    window,
3068                    cx,
3069                );
3070                editor.insert_thread_summary(
3071                    acp::SessionId::new("thread-123"),
3072                    Some("Previous Conversation".into()),
3073                    window,
3074                    cx,
3075                );
3076                editor
3077            })
3078        });
3079
3080        message_editor.read_with(cx, |editor, cx| {
3081            assert!(
3082                editor.text(cx).is_empty(),
3083                "Expected thread summary to be skipped for external agents"
3084            );
3085            assert!(
3086                editor.mention_set().read(cx).mentions().is_empty(),
3087                "Expected no mentions when thread summary is skipped"
3088            );
3089        });
3090    }
3091
3092    #[gpui::test]
3093    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
3094        init_test(cx);
3095
3096        let fs = FakeFs::new(cx.executor());
3097        fs.insert_tree("/project", json!({"file": ""})).await;
3098        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3099
3100        let (multi_workspace, cx) =
3101            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3102        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3103
3104        let thread_store = None;
3105
3106        let message_editor = cx.update(|window, cx| {
3107            cx.new(|cx| {
3108                MessageEditor::new(
3109                    workspace.downgrade(),
3110                    project.downgrade(),
3111                    thread_store.clone(),
3112                    None,
3113                    None,
3114                    Default::default(),
3115                    "Test Agent".into(),
3116                    "Test",
3117                    EditorMode::AutoHeight {
3118                        min_lines: 1,
3119                        max_lines: None,
3120                    },
3121                    window,
3122                    cx,
3123                )
3124            })
3125        });
3126
3127        message_editor.update(cx, |editor, _cx| {
3128            editor
3129                .session_capabilities
3130                .write()
3131                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3132        });
3133
3134        let supported_modes = {
3135            let app = cx.app.borrow();
3136            let _ = &app;
3137            message_editor
3138                .read(&app)
3139                .session_capabilities
3140                .read()
3141                .supported_modes(false)
3142        };
3143
3144        assert!(
3145            !supported_modes.contains(&PromptContextType::Thread),
3146            "Expected thread mode to be hidden when thread mentions are disabled"
3147        );
3148    }
3149
3150    #[gpui::test]
3151    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
3152        init_test(cx);
3153
3154        let fs = FakeFs::new(cx.executor());
3155        fs.insert_tree("/project", json!({"file": ""})).await;
3156        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3157
3158        let (multi_workspace, cx) =
3159            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3160        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3161
3162        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3163
3164        let message_editor = cx.update(|window, cx| {
3165            cx.new(|cx| {
3166                MessageEditor::new(
3167                    workspace.downgrade(),
3168                    project.downgrade(),
3169                    thread_store.clone(),
3170                    None,
3171                    None,
3172                    Default::default(),
3173                    "Test Agent".into(),
3174                    "Test",
3175                    EditorMode::AutoHeight {
3176                        min_lines: 1,
3177                        max_lines: None,
3178                    },
3179                    window,
3180                    cx,
3181                )
3182            })
3183        });
3184
3185        message_editor.update(cx, |editor, _cx| {
3186            editor
3187                .session_capabilities
3188                .write()
3189                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
3190        });
3191
3192        let supported_modes = {
3193            let app = cx.app.borrow();
3194            let _ = &app;
3195            message_editor
3196                .read(&app)
3197                .session_capabilities
3198                .read()
3199                .supported_modes(true)
3200        };
3201
3202        assert!(
3203            supported_modes.contains(&PromptContextType::Thread),
3204            "Expected thread mode to be visible when enabled"
3205        );
3206    }
3207
3208    #[gpui::test]
3209    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
3210        init_test(cx);
3211
3212        let fs = FakeFs::new(cx.executor());
3213        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
3214            .await;
3215        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3216
3217        let (multi_workspace, cx) =
3218            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3219        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3220
3221        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3222
3223        let message_editor = cx.update(|window, cx| {
3224            cx.new(|cx| {
3225                MessageEditor::new(
3226                    workspace.downgrade(),
3227                    project.downgrade(),
3228                    thread_store.clone(),
3229                    None,
3230                    None,
3231                    Default::default(),
3232                    "Test Agent".into(),
3233                    "Test",
3234                    EditorMode::AutoHeight {
3235                        min_lines: 1,
3236                        max_lines: None,
3237                    },
3238                    window,
3239                    cx,
3240                )
3241            })
3242        });
3243        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
3244
3245        cx.run_until_parked();
3246
3247        editor.update_in(cx, |editor, window, cx| {
3248            editor.set_text("  \u{A0}してhello world  ", window, cx);
3249        });
3250
3251        let (content, _) = message_editor
3252            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
3253            .await
3254            .unwrap();
3255
3256        assert_eq!(content, vec!["してhello world".into()]);
3257    }
3258
3259    #[gpui::test]
3260    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
3261        init_test(cx);
3262
3263        let fs = FakeFs::new(cx.executor());
3264
3265        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
3266
3267        fs.insert_tree(
3268            "/project",
3269            json!({
3270                "src": {
3271                    "main.rs": file_content,
3272                }
3273            }),
3274        )
3275        .await;
3276
3277        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3278
3279        let (multi_workspace, cx) =
3280            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3281        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3282
3283        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3284
3285        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
3286            let workspace_handle = cx.weak_entity();
3287            let message_editor = cx.new(|cx| {
3288                MessageEditor::new(
3289                    workspace_handle,
3290                    project.downgrade(),
3291                    thread_store.clone(),
3292                    None,
3293                    None,
3294                    Default::default(),
3295                    "Test Agent".into(),
3296                    "Test",
3297                    EditorMode::AutoHeight {
3298                        min_lines: 1,
3299                        max_lines: None,
3300                    },
3301                    window,
3302                    cx,
3303                )
3304            });
3305            workspace.active_pane().update(cx, |pane, cx| {
3306                pane.add_item(
3307                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3308                    true,
3309                    true,
3310                    None,
3311                    window,
3312                    cx,
3313                );
3314            });
3315            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3316            let editor = message_editor.read(cx).editor().clone();
3317            (message_editor, editor)
3318        });
3319
3320        cx.simulate_input("What is in @file main");
3321
3322        editor.update_in(cx, |editor, window, cx| {
3323            assert!(editor.has_visible_completions_menu());
3324            assert_eq!(editor.text(cx), "What is in @file main");
3325            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
3326        });
3327
3328        let content = message_editor
3329            .update(cx, |editor, cx| editor.contents(false, cx))
3330            .await
3331            .unwrap()
3332            .0;
3333
3334        let main_rs_uri = if cfg!(windows) {
3335            "file:///C:/project/src/main.rs"
3336        } else {
3337            "file:///project/src/main.rs"
3338        };
3339
3340        // When embedded context is `false` we should get a resource link
3341        pretty_assertions::assert_eq!(
3342            content,
3343            vec![
3344                "What is in ".into(),
3345                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
3346            ]
3347        );
3348
3349        message_editor.update(cx, |editor, _cx| {
3350            editor
3351                .session_capabilities
3352                .write()
3353                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true))
3354        });
3355
3356        let content = message_editor
3357            .update(cx, |editor, cx| editor.contents(false, cx))
3358            .await
3359            .unwrap()
3360            .0;
3361
3362        // When embedded context is `true` we should get a resource
3363        pretty_assertions::assert_eq!(
3364            content,
3365            vec![
3366                "What is in ".into(),
3367                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
3368                    acp::EmbeddedResourceResource::TextResourceContents(
3369                        acp::TextResourceContents::new(file_content, main_rs_uri)
3370                    )
3371                ))
3372            ]
3373        );
3374    }
3375
3376    #[gpui::test]
3377    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
3378        init_test(cx);
3379
3380        let app_state = cx.update(AppState::test);
3381
3382        cx.update(|cx| {
3383            editor::init(cx);
3384            workspace::init(app_state.clone(), cx);
3385        });
3386
3387        app_state
3388            .fs
3389            .as_fake()
3390            .insert_tree(
3391                path!("/dir"),
3392                json!({
3393                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
3394                }),
3395            )
3396            .await;
3397
3398        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3399        let window =
3400            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3401        let workspace = window
3402            .read_with(cx, |mw, _| mw.workspace().clone())
3403            .unwrap();
3404
3405        let worktree = project.update(cx, |project, cx| {
3406            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
3407            assert_eq!(worktrees.len(), 1);
3408            worktrees.pop().unwrap()
3409        });
3410        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
3411
3412        let mut cx = VisualTestContext::from_window(window.into(), cx);
3413
3414        // Open a regular editor with the created file, and select a portion of
3415        // the text that will be used for the selections that are meant to be
3416        // inserted in the agent panel.
3417        let editor = workspace
3418            .update_in(&mut cx, |workspace, window, cx| {
3419                workspace.open_path(
3420                    ProjectPath {
3421                        worktree_id,
3422                        path: rel_path("test.txt").into(),
3423                    },
3424                    None,
3425                    false,
3426                    window,
3427                    cx,
3428                )
3429            })
3430            .await
3431            .unwrap()
3432            .downcast::<Editor>()
3433            .unwrap();
3434
3435        editor.update_in(&mut cx, |editor, window, cx| {
3436            editor.change_selections(Default::default(), window, cx, |selections| {
3437                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
3438            });
3439        });
3440
3441        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3442
3443        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
3444        // to ensure we have a fixed viewport, so we can eventually actually
3445        // place the cursor outside of the visible area.
3446        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
3447            let workspace_handle = cx.weak_entity();
3448            let message_editor = cx.new(|cx| {
3449                MessageEditor::new(
3450                    workspace_handle,
3451                    project.downgrade(),
3452                    thread_store.clone(),
3453                    None,
3454                    None,
3455                    Default::default(),
3456                    "Test Agent".into(),
3457                    "Test",
3458                    EditorMode::full(),
3459                    window,
3460                    cx,
3461                )
3462            });
3463            workspace.active_pane().update(cx, |pane, cx| {
3464                pane.add_item(
3465                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3466                    true,
3467                    true,
3468                    None,
3469                    window,
3470                    cx,
3471                );
3472            });
3473
3474            message_editor
3475        });
3476
3477        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3478            message_editor.editor.update(cx, |editor, cx| {
3479                // Update the Agent Panel's Message Editor text to have 100
3480                // lines, ensuring that the cursor is set at line 90 and that we
3481                // then scroll all the way to the top, so the cursor's position
3482                // remains off screen.
3483                let mut lines = String::new();
3484                for _ in 1..=100 {
3485                    lines.push_str(&"Another line in the agent panel's message editor\n");
3486                }
3487                editor.set_text(lines.as_str(), window, cx);
3488                editor.change_selections(Default::default(), window, cx, |selections| {
3489                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
3490                });
3491                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
3492            });
3493        });
3494
3495        cx.run_until_parked();
3496
3497        // Before proceeding, let's assert that the cursor is indeed off screen,
3498        // otherwise the rest of the test doesn't make sense.
3499        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3500            message_editor.editor.update(cx, |editor, cx| {
3501                let snapshot = editor.snapshot(window, cx);
3502                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3503                let scroll_top = snapshot.scroll_position().y as u32;
3504                let visible_lines = editor.visible_line_count().unwrap() as u32;
3505                let visible_range = scroll_top..(scroll_top + visible_lines);
3506
3507                assert!(!visible_range.contains(&cursor_row));
3508            })
3509        });
3510
3511        // Now let's insert the selection in the Agent Panel's editor and
3512        // confirm that, after the insertion, the cursor is now in the visible
3513        // range.
3514        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3515            message_editor.insert_selections(window, cx);
3516        });
3517
3518        cx.run_until_parked();
3519
3520        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3521            message_editor.editor.update(cx, |editor, cx| {
3522                let snapshot = editor.snapshot(window, cx);
3523                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3524                let scroll_top = snapshot.scroll_position().y as u32;
3525                let visible_lines = editor.visible_line_count().unwrap() as u32;
3526                let visible_range = scroll_top..(scroll_top + visible_lines);
3527
3528                assert!(visible_range.contains(&cursor_row));
3529            })
3530        });
3531    }
3532
3533    #[gpui::test]
3534    async fn test_insert_context_with_multibyte_characters(cx: &mut TestAppContext) {
3535        init_test(cx);
3536
3537        let app_state = cx.update(AppState::test);
3538
3539        cx.update(|cx| {
3540            editor::init(cx);
3541            workspace::init(app_state.clone(), cx);
3542        });
3543
3544        app_state
3545            .fs
3546            .as_fake()
3547            .insert_tree(path!("/dir"), json!({}))
3548            .await;
3549
3550        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3551        let window =
3552            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3553        let workspace = window
3554            .read_with(cx, |mw, _| mw.workspace().clone())
3555            .unwrap();
3556
3557        let mut cx = VisualTestContext::from_window(window.into(), cx);
3558
3559        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3560
3561        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3562            let workspace_handle = cx.weak_entity();
3563            let message_editor = cx.new(|cx| {
3564                MessageEditor::new(
3565                    workspace_handle,
3566                    project.downgrade(),
3567                    Some(thread_store.clone()),
3568                    None,
3569                    None,
3570                    Default::default(),
3571                    "Test Agent".into(),
3572                    "Test",
3573                    EditorMode::AutoHeight {
3574                        max_lines: None,
3575                        min_lines: 1,
3576                    },
3577                    window,
3578                    cx,
3579                )
3580            });
3581            workspace.active_pane().update(cx, |pane, cx| {
3582                pane.add_item(
3583                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3584                    true,
3585                    true,
3586                    None,
3587                    window,
3588                    cx,
3589                );
3590            });
3591            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3592            let editor = message_editor.read(cx).editor().clone();
3593            (message_editor, editor)
3594        });
3595
3596        editor.update_in(&mut cx, |editor, window, cx| {
3597            editor.set_text("😄😄", window, cx);
3598        });
3599
3600        cx.run_until_parked();
3601
3602        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3603            message_editor.insert_context_type("file", window, cx);
3604        });
3605
3606        cx.run_until_parked();
3607
3608        editor.update(&mut cx, |editor, cx| {
3609            assert_eq!(editor.text(cx), "😄😄@file");
3610        });
3611    }
3612
3613    #[gpui::test]
3614    async fn test_paste_mention_link_with_multiple_selections(cx: &mut TestAppContext) {
3615        init_test(cx);
3616
3617        let app_state = cx.update(AppState::test);
3618
3619        cx.update(|cx| {
3620            editor::init(cx);
3621            workspace::init(app_state.clone(), cx);
3622        });
3623
3624        app_state
3625            .fs
3626            .as_fake()
3627            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3628            .await;
3629
3630        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3631        let window =
3632            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3633        let workspace = window
3634            .read_with(cx, |mw, _| mw.workspace().clone())
3635            .unwrap();
3636
3637        let mut cx = VisualTestContext::from_window(window.into(), cx);
3638
3639        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3640
3641        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3642            let workspace_handle = cx.weak_entity();
3643            let message_editor = cx.new(|cx| {
3644                MessageEditor::new(
3645                    workspace_handle,
3646                    project.downgrade(),
3647                    Some(thread_store),
3648                    None,
3649                    None,
3650                    Default::default(),
3651                    "Test Agent".into(),
3652                    "Test",
3653                    EditorMode::AutoHeight {
3654                        max_lines: None,
3655                        min_lines: 1,
3656                    },
3657                    window,
3658                    cx,
3659                )
3660            });
3661            workspace.active_pane().update(cx, |pane, cx| {
3662                pane.add_item(
3663                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3664                    true,
3665                    true,
3666                    None,
3667                    window,
3668                    cx,
3669                );
3670            });
3671            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3672            let editor = message_editor.read(cx).editor().clone();
3673            (message_editor, editor)
3674        });
3675
3676        editor.update_in(&mut cx, |editor, window, cx| {
3677            editor.set_text(
3678                "AAAAAAAAAAAAAAAAAAAAAAAAA     AAAAAAAAAAAAAAAAAAAAAAAAA",
3679                window,
3680                cx,
3681            );
3682        });
3683
3684        cx.run_until_parked();
3685
3686        editor.update_in(&mut cx, |editor, window, cx| {
3687            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3688                s.select_ranges([
3689                    MultiBufferOffset(0)..MultiBufferOffset(25), // First selection (large)
3690                    MultiBufferOffset(30)..MultiBufferOffset(55), // Second selection (newest)
3691                ]);
3692            });
3693        });
3694
3695        let mention_link = "[@f](file:///test.txt)";
3696        cx.write_to_clipboard(ClipboardItem::new_string(mention_link.into()));
3697
3698        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3699            message_editor.paste(&Paste, window, cx);
3700        });
3701
3702        let text = editor.update(&mut cx, |editor, cx| editor.text(cx));
3703        assert!(
3704            text.contains("[@f](file:///test.txt)"),
3705            "Expected mention link to be pasted, got: {}",
3706            text
3707        );
3708    }
3709
3710    #[gpui::test]
3711    async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
3712        cx: &mut TestAppContext,
3713    ) {
3714        init_test(cx);
3715
3716        let app_state = cx.update(AppState::test);
3717
3718        cx.update(|cx| {
3719            editor::init(cx);
3720            workspace::init(app_state.clone(), cx);
3721        });
3722
3723        app_state
3724            .fs
3725            .as_fake()
3726            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3727            .await;
3728
3729        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3730        let window =
3731            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3732        let workspace = window
3733            .read_with(cx, |mw, _| mw.workspace().clone())
3734            .unwrap();
3735
3736        let mut cx = VisualTestContext::from_window(window.into(), cx);
3737
3738        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3739
3740        let (_message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3741            let workspace_handle = cx.weak_entity();
3742            let message_editor = cx.new(|cx| {
3743                MessageEditor::new(
3744                    workspace_handle,
3745                    project.downgrade(),
3746                    Some(thread_store),
3747                    None,
3748                    None,
3749                    Default::default(),
3750                    "Test Agent".into(),
3751                    "Test",
3752                    EditorMode::AutoHeight {
3753                        max_lines: None,
3754                        min_lines: 1,
3755                    },
3756                    window,
3757                    cx,
3758                )
3759            });
3760            workspace.active_pane().update(cx, |pane, cx| {
3761                pane.add_item(
3762                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3763                    true,
3764                    true,
3765                    None,
3766                    window,
3767                    cx,
3768                );
3769            });
3770            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3771            let editor = message_editor.read(cx).editor().clone();
3772            (message_editor, editor)
3773        });
3774
3775        cx.simulate_input("@");
3776
3777        editor.update(&mut cx, |editor, cx| {
3778            assert_eq!(editor.text(cx), "@");
3779            assert!(editor.has_visible_completions_menu());
3780        });
3781
3782        cx.write_to_clipboard(ClipboardItem::new_string("[@f](file:///test.txt) @".into()));
3783        cx.dispatch_action(Paste);
3784
3785        editor.update(&mut cx, |editor, cx| {
3786            assert!(editor.text(cx).contains("[@f](file:///test.txt)"));
3787        });
3788    }
3789
3790    // Helper that creates a minimal MessageEditor inside a window, returning both
3791    // the entity and the underlying VisualTestContext so callers can drive updates.
3792    async fn setup_message_editor(
3793        cx: &mut TestAppContext,
3794    ) -> (Entity<MessageEditor>, &mut VisualTestContext) {
3795        let fs = FakeFs::new(cx.executor());
3796        fs.insert_tree("/project", json!({"file.txt": ""})).await;
3797        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3798
3799        let (multi_workspace, cx) =
3800            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3801        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3802
3803        let message_editor = cx.update(|window, cx| {
3804            cx.new(|cx| {
3805                MessageEditor::new(
3806                    workspace.downgrade(),
3807                    project.downgrade(),
3808                    None,
3809                    None,
3810                    None,
3811                    Default::default(),
3812                    "Test Agent".into(),
3813                    "Test",
3814                    EditorMode::AutoHeight {
3815                        min_lines: 1,
3816                        max_lines: None,
3817                    },
3818                    window,
3819                    cx,
3820                )
3821            })
3822        });
3823
3824        cx.run_until_parked();
3825        (message_editor, cx)
3826    }
3827
3828    #[gpui::test]
3829    async fn test_set_message_plain_text(cx: &mut TestAppContext) {
3830        init_test(cx);
3831        let (message_editor, cx) = setup_message_editor(cx).await;
3832
3833        message_editor.update_in(cx, |editor, window, cx| {
3834            editor.set_message(
3835                vec![acp::ContentBlock::Text(acp::TextContent::new(
3836                    "hello world".to_string(),
3837                ))],
3838                window,
3839                cx,
3840            );
3841        });
3842
3843        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3844        assert_eq!(text, "hello world");
3845        assert!(!message_editor.update(cx, |editor, cx| editor.is_empty(cx)));
3846    }
3847
3848    #[gpui::test]
3849    async fn test_set_message_replaces_existing_content(cx: &mut TestAppContext) {
3850        init_test(cx);
3851        let (message_editor, cx) = setup_message_editor(cx).await;
3852
3853        // Set initial content.
3854        message_editor.update_in(cx, |editor, window, cx| {
3855            editor.set_message(
3856                vec![acp::ContentBlock::Text(acp::TextContent::new(
3857                    "old content".to_string(),
3858                ))],
3859                window,
3860                cx,
3861            );
3862        });
3863
3864        // Replace with new content.
3865        message_editor.update_in(cx, |editor, window, cx| {
3866            editor.set_message(
3867                vec![acp::ContentBlock::Text(acp::TextContent::new(
3868                    "new content".to_string(),
3869                ))],
3870                window,
3871                cx,
3872            );
3873        });
3874
3875        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3876        assert_eq!(
3877            text, "new content",
3878            "set_message should replace old content"
3879        );
3880    }
3881
3882    #[gpui::test]
3883    async fn test_append_message_to_empty_editor(cx: &mut TestAppContext) {
3884        init_test(cx);
3885        let (message_editor, cx) = setup_message_editor(cx).await;
3886
3887        message_editor.update_in(cx, |editor, window, cx| {
3888            editor.append_message(
3889                vec![acp::ContentBlock::Text(acp::TextContent::new(
3890                    "appended".to_string(),
3891                ))],
3892                Some("\n\n"),
3893                window,
3894                cx,
3895            );
3896        });
3897
3898        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3899        assert_eq!(
3900            text, "appended",
3901            "No separator should be inserted when the editor is empty"
3902        );
3903    }
3904
3905    #[gpui::test]
3906    async fn test_append_message_to_non_empty_editor(cx: &mut TestAppContext) {
3907        init_test(cx);
3908        let (message_editor, cx) = setup_message_editor(cx).await;
3909
3910        // Seed initial content.
3911        message_editor.update_in(cx, |editor, window, cx| {
3912            editor.set_message(
3913                vec![acp::ContentBlock::Text(acp::TextContent::new(
3914                    "initial".to_string(),
3915                ))],
3916                window,
3917                cx,
3918            );
3919        });
3920
3921        // Append with separator.
3922        message_editor.update_in(cx, |editor, window, cx| {
3923            editor.append_message(
3924                vec![acp::ContentBlock::Text(acp::TextContent::new(
3925                    "appended".to_string(),
3926                ))],
3927                Some("\n\n"),
3928                window,
3929                cx,
3930            );
3931        });
3932
3933        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3934        assert_eq!(
3935            text, "initial\n\nappended",
3936            "Separator should appear between existing and appended content"
3937        );
3938    }
3939
3940    #[gpui::test]
3941    async fn test_append_message_preserves_mention_offset(cx: &mut TestAppContext) {
3942        init_test(cx);
3943
3944        let fs = FakeFs::new(cx.executor());
3945        fs.insert_tree("/project", json!({"file.txt": "content"}))
3946            .await;
3947        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3948
3949        let (multi_workspace, cx) =
3950            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3951        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3952
3953        let message_editor = cx.update(|window, cx| {
3954            cx.new(|cx| {
3955                MessageEditor::new(
3956                    workspace.downgrade(),
3957                    project.downgrade(),
3958                    None,
3959                    None,
3960                    None,
3961                    Default::default(),
3962                    "Test Agent".into(),
3963                    "Test",
3964                    EditorMode::AutoHeight {
3965                        min_lines: 1,
3966                        max_lines: None,
3967                    },
3968                    window,
3969                    cx,
3970                )
3971            })
3972        });
3973
3974        cx.run_until_parked();
3975
3976        // Seed plain-text prefix so the editor is non-empty before appending.
3977        message_editor.update_in(cx, |editor, window, cx| {
3978            editor.set_message(
3979                vec![acp::ContentBlock::Text(acp::TextContent::new(
3980                    "prefix text".to_string(),
3981                ))],
3982                window,
3983                cx,
3984            );
3985        });
3986
3987        // Append a message that contains a ResourceLink mention.
3988        message_editor.update_in(cx, |editor, window, cx| {
3989            editor.append_message(
3990                vec![acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
3991                    "file.txt",
3992                    "file:///project/file.txt",
3993                ))],
3994                Some("\n\n"),
3995                window,
3996                cx,
3997            );
3998        });
3999
4000        cx.run_until_parked();
4001
4002        // The mention should be registered in the mention_set so that contents()
4003        // will emit it as a structured block rather than plain text.
4004        let mention_uris =
4005            message_editor.update(cx, |editor, cx| editor.mention_set.read(cx).mentions());
4006        assert_eq!(
4007            mention_uris.len(),
4008            1,
4009            "Expected exactly one mention in the mention_set after append, got: {mention_uris:?}"
4010        );
4011
4012        // The editor text should start with the prefix, then the separator, then
4013        // the mention placeholder — confirming the offset was computed correctly.
4014        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
4015        assert!(
4016            text.starts_with("prefix text\n\n"),
4017            "Expected text to start with 'prefix text\\n\\n', got: {text:?}"
4018        );
4019    }
4020}