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