message_editor.rs

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