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