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