message_editor.rs

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