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