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