message_editor.rs

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