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