message_editor.rs

   1use crate::SendImmediately;
   2use crate::acp::AcpThreadHistory;
   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    user_slash_command::{self, CommandLoadError, UserSlashCommand},
  13};
  14use acp_thread::{AgentSessionInfo, MentionUri};
  15use agent::ThreadStore;
  16use agent_client_protocol as acp;
  17use anyhow::{Result, anyhow};
  18use collections::HashSet;
  19use editor::{
  20    Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
  21    EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset,
  22    MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu,
  23    scroll::Autoscroll,
  24};
  25use feature_flags::{FeatureFlagAppExt as _, UserSlashCommandsFeatureFlag};
  26use futures::{FutureExt as _, future::join_all};
  27use gpui::{
  28    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
  29    KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
  30};
  31use language::{Buffer, Language, language_settings::InlayHintKind};
  32use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
  33use prompt_store::PromptStore;
  34use rope::Point;
  35use settings::Settings;
  36use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
  37use theme::ThemeSettings;
  38use ui::{ButtonLike, ButtonStyle, ContextMenu, Disclosure, ElevationIndex, prelude::*};
  39use util::{ResultExt, debug_panic};
  40use workspace::{CollaboratorId, Workspace};
  41use zed_actions::agent::{Chat, PasteRaw};
  42
  43enum UserSlashCommands {
  44    Cached {
  45        commands: collections::HashMap<String, user_slash_command::UserSlashCommand>,
  46        errors: Vec<user_slash_command::CommandLoadError>,
  47    },
  48    FromFs {
  49        fs: Arc<dyn fs::Fs>,
  50        worktree_roots: Vec<std::path::PathBuf>,
  51    },
  52}
  53
  54pub struct MessageEditor {
  55    mention_set: Entity<MentionSet>,
  56    editor: Entity<Editor>,
  57    workspace: WeakEntity<Workspace>,
  58    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  59    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  60    cached_user_commands: Rc<RefCell<collections::HashMap<String, UserSlashCommand>>>,
  61    cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
  62    agent_name: SharedString,
  63    thread_store: Option<Entity<ThreadStore>>,
  64    _subscriptions: Vec<Subscription>,
  65    _parse_slash_command_task: Task<()>,
  66}
  67
  68#[derive(Clone, Copy, Debug)]
  69pub enum MessageEditorEvent {
  70    Send,
  71    SendImmediately,
  72    Cancel,
  73    Focus,
  74    LostFocus,
  75}
  76
  77impl EventEmitter<MessageEditorEvent> for MessageEditor {}
  78
  79const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
  80
  81impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
  82    fn supports_images(&self, cx: &App) -> bool {
  83        self.read(cx).prompt_capabilities.borrow().image
  84    }
  85
  86    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
  87        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
  88        if self.read(cx).prompt_capabilities.borrow().embedded_context {
  89            if self.read(cx).thread_store.is_some() {
  90                supported.push(PromptContextType::Thread);
  91            }
  92            supported.extend(&[
  93                PromptContextType::Diagnostics,
  94                PromptContextType::Fetch,
  95                PromptContextType::Rules,
  96            ]);
  97        }
  98        supported
  99    }
 100
 101    fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
 102        self.read(cx)
 103            .available_commands
 104            .borrow()
 105            .iter()
 106            .map(|cmd| crate::completion_provider::AvailableCommand {
 107                name: cmd.name.clone().into(),
 108                description: cmd.description.clone().into(),
 109                requires_argument: cmd.input.is_some(),
 110                source: crate::completion_provider::CommandSource::Server,
 111            })
 112            .collect()
 113    }
 114
 115    fn confirm_command(&self, cx: &mut App) {
 116        self.update(cx, |this, cx| this.send(cx));
 117    }
 118
 119    fn cached_user_commands(
 120        &self,
 121        cx: &App,
 122    ) -> Option<collections::HashMap<String, UserSlashCommand>> {
 123        let commands = self.read(cx).cached_user_commands.borrow();
 124        if commands.is_empty() {
 125            None
 126        } else {
 127            Some(commands.clone())
 128        }
 129    }
 130
 131    fn cached_user_command_errors(&self, cx: &App) -> Option<Vec<CommandLoadError>> {
 132        let errors = self.read(cx).cached_user_command_errors.borrow();
 133        if errors.is_empty() {
 134            None
 135        } else {
 136            Some(errors.clone())
 137        }
 138    }
 139}
 140
 141impl MessageEditor {
 142    pub fn new(
 143        workspace: WeakEntity<Workspace>,
 144        project: WeakEntity<Project>,
 145        thread_store: Option<Entity<ThreadStore>>,
 146        history: WeakEntity<AcpThreadHistory>,
 147        prompt_store: Option<Entity<PromptStore>>,
 148        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
 149        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 150        agent_name: SharedString,
 151        placeholder: &str,
 152        mode: EditorMode,
 153        window: &mut Window,
 154        cx: &mut Context<Self>,
 155    ) -> Self {
 156        let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
 157        let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
 158        Self::new_with_cache(
 159            workspace,
 160            project,
 161            thread_store,
 162            history,
 163            prompt_store,
 164            prompt_capabilities,
 165            available_commands,
 166            cached_user_commands,
 167            cached_user_command_errors,
 168            agent_name,
 169            placeholder,
 170            mode,
 171            window,
 172            cx,
 173        )
 174    }
 175
 176    pub fn new_with_cache(
 177        workspace: WeakEntity<Workspace>,
 178        project: WeakEntity<Project>,
 179        thread_store: Option<Entity<ThreadStore>>,
 180        history: WeakEntity<AcpThreadHistory>,
 181        prompt_store: Option<Entity<PromptStore>>,
 182        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
 183        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 184        cached_user_commands: Rc<RefCell<collections::HashMap<String, UserSlashCommand>>>,
 185        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
 186        agent_name: SharedString,
 187        placeholder: &str,
 188        mode: EditorMode,
 189        window: &mut Window,
 190        cx: &mut Context<Self>,
 191    ) -> Self {
 192        let language = Language::new(
 193            language::LanguageConfig {
 194                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 195                ..Default::default()
 196            },
 197            None,
 198        );
 199
 200        let editor = cx.new(|cx| {
 201            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 202            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 203
 204            let mut editor = Editor::new(mode, buffer, None, window, cx);
 205            editor.set_placeholder_text(placeholder, window, cx);
 206            editor.set_show_indent_guides(false, cx);
 207            editor.set_show_completions_on_input(Some(true));
 208            editor.set_soft_wrap();
 209            editor.set_use_modal_editing(true);
 210            editor.set_context_menu_options(ContextMenuOptions {
 211                min_entries_visible: 12,
 212                max_entries_visible: 12,
 213                placement: Some(ContextMenuPlacement::Above),
 214            });
 215            editor.register_addon(MessageEditorAddon::new());
 216
 217            editor.set_custom_context_menu(|editor, _point, window, cx| {
 218                let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
 219
 220                Some(ContextMenu::build(window, cx, |menu, _, _| {
 221                    menu.action("Cut", Box::new(editor::actions::Cut))
 222                        .action_disabled_when(
 223                            !has_selection,
 224                            "Copy",
 225                            Box::new(editor::actions::Copy),
 226                        )
 227                        .action("Paste", Box::new(editor::actions::Paste))
 228                }))
 229            });
 230
 231            editor
 232        });
 233        let mention_set =
 234            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
 235        let completion_provider = Rc::new(PromptCompletionProvider::new(
 236            cx.entity(),
 237            editor.downgrade(),
 238            mention_set.clone(),
 239            history,
 240            prompt_store.clone(),
 241            workspace.clone(),
 242        ));
 243        editor.update(cx, |editor, _cx| {
 244            editor.set_completion_provider(Some(completion_provider.clone()))
 245        });
 246
 247        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 248            cx.emit(MessageEditorEvent::Focus)
 249        })
 250        .detach();
 251        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 252            cx.emit(MessageEditorEvent::LostFocus)
 253        })
 254        .detach();
 255
 256        let mut has_hint = false;
 257        let mut subscriptions = Vec::new();
 258
 259        subscriptions.push(cx.subscribe_in(&editor, window, {
 260            move |this, editor, event, window, cx| {
 261                if let EditorEvent::Edited { .. } = event
 262                    && !editor.read(cx).read_only(cx)
 263                {
 264                    editor.update(cx, |editor, cx| {
 265                        let snapshot = editor.snapshot(window, cx);
 266                        this.mention_set
 267                            .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 268
 269                        let new_hints = this
 270                            .command_hint(snapshot.buffer())
 271                            .into_iter()
 272                            .collect::<Vec<_>>();
 273                        let has_new_hint = !new_hints.is_empty();
 274                        editor.splice_inlays(
 275                            if has_hint {
 276                                &[COMMAND_HINT_INLAY_ID]
 277                            } else {
 278                                &[]
 279                            },
 280                            new_hints,
 281                            cx,
 282                        );
 283                        has_hint = has_new_hint;
 284                    });
 285                    cx.notify();
 286                }
 287            }
 288        }));
 289
 290        Self {
 291            editor,
 292            mention_set,
 293            workspace,
 294            prompt_capabilities,
 295            available_commands,
 296            cached_user_commands,
 297            cached_user_command_errors,
 298            agent_name,
 299            thread_store,
 300            _subscriptions: subscriptions,
 301            _parse_slash_command_task: Task::ready(()),
 302        }
 303    }
 304
 305    fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
 306        let available_commands = self.available_commands.borrow();
 307        if available_commands.is_empty() {
 308            return None;
 309        }
 310
 311        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 312        if parsed_command.argument.is_some() {
 313            return None;
 314        }
 315
 316        let command_name = parsed_command.command?;
 317        let available_command = available_commands
 318            .iter()
 319            .find(|command| command.name == command_name)?;
 320
 321        let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
 322            mut hint,
 323            ..
 324        }) = available_command.input.clone()?
 325        else {
 326            return None;
 327        };
 328
 329        let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
 330        if hint_pos > snapshot.len() {
 331            hint_pos = snapshot.len();
 332            hint.insert(0, ' ');
 333        }
 334
 335        let hint_pos = snapshot.anchor_after(hint_pos);
 336
 337        Some(Inlay::hint(
 338            COMMAND_HINT_INLAY_ID,
 339            hint_pos,
 340            &InlayHint {
 341                position: hint_pos.text_anchor,
 342                label: InlayHintLabel::String(hint),
 343                kind: Some(InlayHintKind::Parameter),
 344                padding_left: false,
 345                padding_right: false,
 346                tooltip: None,
 347                resolve_state: project::ResolveState::Resolved,
 348            },
 349        ))
 350    }
 351
 352    pub fn insert_thread_summary(
 353        &mut self,
 354        thread: AgentSessionInfo,
 355        window: &mut Window,
 356        cx: &mut Context<Self>,
 357    ) {
 358        if self.thread_store.is_none() {
 359            return;
 360        }
 361        let Some(workspace) = self.workspace.upgrade() else {
 362            return;
 363        };
 364        let thread_title = thread
 365            .title
 366            .clone()
 367            .filter(|title| !title.is_empty())
 368            .unwrap_or_else(|| SharedString::new_static("New Thread"));
 369        let uri = MentionUri::Thread {
 370            id: thread.session_id,
 371            name: thread_title.to_string(),
 372        };
 373        let content = format!("{}\n", uri.as_link());
 374
 375        let content_len = content.len() - 1;
 376
 377        let start = self.editor.update(cx, |editor, cx| {
 378            editor.set_text(content, window, cx);
 379            editor
 380                .buffer()
 381                .read(cx)
 382                .snapshot(cx)
 383                .anchor_before(Point::zero())
 384                .text_anchor
 385        });
 386
 387        let supports_images = self.prompt_capabilities.borrow().image;
 388
 389        self.mention_set
 390            .update(cx, |mention_set, cx| {
 391                mention_set.confirm_mention_completion(
 392                    thread_title,
 393                    start,
 394                    content_len,
 395                    uri,
 396                    supports_images,
 397                    self.editor.clone(),
 398                    &workspace,
 399                    window,
 400                    cx,
 401                )
 402            })
 403            .detach();
 404    }
 405
 406    #[cfg(test)]
 407    pub(crate) fn editor(&self) -> &Entity<Editor> {
 408        &self.editor
 409    }
 410
 411    pub fn is_empty(&self, cx: &App) -> bool {
 412        self.editor.read(cx).is_empty(cx)
 413    }
 414
 415    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 416        self.editor
 417            .read(cx)
 418            .context_menu()
 419            .borrow()
 420            .as_ref()
 421            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 422    }
 423
 424    #[cfg(test)]
 425    pub fn mention_set(&self) -> &Entity<MentionSet> {
 426        &self.mention_set
 427    }
 428
 429    fn validate_slash_commands(
 430        text: &str,
 431        available_commands: &[acp::AvailableCommand],
 432        agent_name: &str,
 433    ) -> Result<()> {
 434        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 435            if let Some(command_name) = parsed_command.command {
 436                // Check if this command is in the list of available commands from the server
 437                let is_supported = available_commands
 438                    .iter()
 439                    .any(|cmd| cmd.name == command_name);
 440
 441                if !is_supported {
 442                    return Err(anyhow!(
 443                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 444                        command_name,
 445                        agent_name,
 446                        if available_commands.is_empty() {
 447                            "none".to_string()
 448                        } else {
 449                            available_commands
 450                                .iter()
 451                                .map(|cmd| format!("/{}", cmd.name))
 452                                .collect::<Vec<_>>()
 453                                .join(", ")
 454                        }
 455                    ));
 456                }
 457            }
 458        }
 459        Ok(())
 460    }
 461
 462    pub fn contents(
 463        &self,
 464        full_mention_content: bool,
 465        cx: &mut Context<Self>,
 466    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 467        self.contents_with_cache(full_mention_content, None, None, cx)
 468    }
 469
 470    pub fn contents_with_cache(
 471        &self,
 472        full_mention_content: bool,
 473        cached_user_commands: Option<
 474            collections::HashMap<String, user_slash_command::UserSlashCommand>,
 475        >,
 476        cached_user_command_errors: Option<Vec<user_slash_command::CommandLoadError>>,
 477        cx: &mut Context<Self>,
 478    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 479        let text = self.editor.read(cx).text(cx);
 480        let available_commands = self.available_commands.borrow().clone();
 481        let agent_name = self.agent_name.clone();
 482
 483        let user_slash_commands = if !cx.has_flag::<UserSlashCommandsFeatureFlag>() {
 484            UserSlashCommands::Cached {
 485                commands: collections::HashMap::default(),
 486                errors: Vec::new(),
 487            }
 488        } else if let Some(cached) = cached_user_commands {
 489            UserSlashCommands::Cached {
 490                commands: cached,
 491                errors: cached_user_command_errors.unwrap_or_default(),
 492            }
 493        } else if let Some(workspace) = self.workspace.upgrade() {
 494            let fs = workspace.read(cx).project().read(cx).fs().clone();
 495            let worktree_roots: Vec<std::path::PathBuf> = workspace
 496                .read(cx)
 497                .visible_worktrees(cx)
 498                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
 499                .collect();
 500            UserSlashCommands::FromFs { fs, worktree_roots }
 501        } else {
 502            UserSlashCommands::Cached {
 503                commands: collections::HashMap::default(),
 504                errors: Vec::new(),
 505            }
 506        };
 507
 508        let contents = self
 509            .mention_set
 510            .update(cx, |store, cx| store.contents(full_mention_content, cx));
 511        let editor = self.editor.clone();
 512        let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
 513
 514        cx.spawn(async move |_, cx| {
 515            let (mut user_commands, mut user_command_errors) = match user_slash_commands {
 516                UserSlashCommands::Cached { commands, errors } => (commands, errors),
 517                UserSlashCommands::FromFs { fs, worktree_roots } => {
 518                    let load_result =
 519                        user_slash_command::load_all_commands_async(&fs, &worktree_roots).await;
 520
 521                    (
 522                        user_slash_command::commands_to_map(&load_result.commands),
 523                        load_result.errors,
 524                    )
 525                }
 526            };
 527
 528            let server_command_names = available_commands
 529                .iter()
 530                .map(|command| command.name.clone())
 531                .collect::<HashSet<_>>();
 532            user_slash_command::apply_server_command_conflicts_to_map(
 533                &mut user_commands,
 534                &mut user_command_errors,
 535                &server_command_names,
 536            );
 537
 538            // Check if the user is trying to use an errored slash command.
 539            // If so, report the error to the user.
 540            if let Some(parsed) = user_slash_command::try_parse_user_command(&text) {
 541                for error in &user_command_errors {
 542                    if let Some(error_cmd_name) = error.command_name() {
 543                        if error_cmd_name == parsed.name {
 544                            return Err(anyhow::anyhow!(
 545                                "Failed to load /{}: {}",
 546                                parsed.name,
 547                                error.message
 548                            ));
 549                        }
 550                    }
 551                }
 552            }
 553            // Errors for commands that don't match the user's input are silently ignored here,
 554            // since the user will see them via the error callout in the thread view.
 555
 556            // Check if this is a user-defined slash command and expand it
 557            match user_slash_command::try_expand_from_commands(&text, &user_commands) {
 558                Ok(Some(expanded)) => return Ok((vec![expanded.into()], Vec::new())),
 559                Err(err) => return Err(err),
 560                Ok(None) => {} // Not a user command, continue with normal processing
 561            }
 562
 563            if let Err(err) = Self::validate_slash_commands(&text, &available_commands, &agent_name)
 564            {
 565                return Err(err);
 566            }
 567
 568            let contents = contents.await?;
 569            let mut all_tracked_buffers = Vec::new();
 570
 571            let result = editor.update(cx, |editor, cx| {
 572                let (mut ix, _) = text
 573                    .char_indices()
 574                    .find(|(_, c)| !c.is_whitespace())
 575                    .unwrap_or((0, '\0'));
 576                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 577                let text = editor.text(cx);
 578                editor.display_map.update(cx, |map, cx| {
 579                    let snapshot = map.snapshot(cx);
 580                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 581                        let Some((uri, mention)) = contents.get(&crease_id) else {
 582                            continue;
 583                        };
 584
 585                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
 586                        if crease_range.start.0 > ix {
 587                            let chunk = text[ix..crease_range.start.0].into();
 588                            chunks.push(chunk);
 589                        }
 590                        let chunk = match mention {
 591                            Mention::Text {
 592                                content,
 593                                tracked_buffers,
 594                            } => {
 595                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 596                                if supports_embedded_context {
 597                                    acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 598                                        acp::EmbeddedResourceResource::TextResourceContents(
 599                                            acp::TextResourceContents::new(
 600                                                content.clone(),
 601                                                uri.to_uri().to_string(),
 602                                            ),
 603                                        ),
 604                                    ))
 605                                } else {
 606                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
 607                                        uri.name(),
 608                                        uri.to_uri().to_string(),
 609                                    ))
 610                                }
 611                            }
 612                            Mention::Image(mention_image) => acp::ContentBlock::Image(
 613                                acp::ImageContent::new(
 614                                    mention_image.data.clone(),
 615                                    mention_image.format.mime_type(),
 616                                )
 617                                .uri(match uri {
 618                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 619                                    MentionUri::PastedImage => None,
 620                                    other => {
 621                                        debug_panic!(
 622                                            "unexpected mention uri for image: {:?}",
 623                                            other
 624                                        );
 625                                        None
 626                                    }
 627                                }),
 628                            ),
 629                            Mention::Link => acp::ContentBlock::ResourceLink(
 630                                acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
 631                            ),
 632                        };
 633                        chunks.push(chunk);
 634                        ix = crease_range.end.0;
 635                    }
 636
 637                    if ix < text.len() {
 638                        let last_chunk = text[ix..].trim_end().to_owned();
 639                        if !last_chunk.is_empty() {
 640                            chunks.push(last_chunk.into());
 641                        }
 642                    }
 643                });
 644                anyhow::Ok((chunks, all_tracked_buffers))
 645            })?;
 646            Ok(result)
 647        })
 648    }
 649
 650    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 651        self.editor.update(cx, |editor, cx| {
 652            editor.clear(window, cx);
 653            editor.remove_creases(
 654                self.mention_set.update(cx, |mention_set, _cx| {
 655                    mention_set
 656                        .clear()
 657                        .map(|(crease_id, _)| crease_id)
 658                        .collect::<Vec<_>>()
 659                }),
 660                cx,
 661            )
 662        });
 663    }
 664
 665    pub fn send(&mut self, cx: &mut Context<Self>) {
 666        if !self.is_empty(cx) {
 667            self.editor.update(cx, |editor, cx| {
 668                editor.clear_inlay_hints(cx);
 669            });
 670        }
 671        cx.emit(MessageEditorEvent::Send)
 672    }
 673
 674    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 675        let editor = self.editor.clone();
 676
 677        cx.spawn_in(window, async move |_, cx| {
 678            editor
 679                .update_in(cx, |editor, window, cx| {
 680                    let menu_is_open =
 681                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 682                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 683                        });
 684
 685                    let has_at_sign = {
 686                        let snapshot = editor.display_snapshot(cx);
 687                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 688                        let offset = cursor.to_offset(&snapshot);
 689                        if offset.0 > 0 {
 690                            snapshot
 691                                .buffer_snapshot()
 692                                .reversed_chars_at(offset)
 693                                .next()
 694                                .map(|sign| sign == '@')
 695                                .unwrap_or(false)
 696                        } else {
 697                            false
 698                        }
 699                    };
 700
 701                    if menu_is_open && has_at_sign {
 702                        return;
 703                    }
 704
 705                    editor.insert("@", window, cx);
 706                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 707                })
 708                .log_err();
 709        })
 710        .detach();
 711    }
 712
 713    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 714        self.send(cx);
 715    }
 716
 717    fn send_immediately(&mut self, _: &SendImmediately, _: &mut Window, cx: &mut Context<Self>) {
 718        if self.is_empty(cx) {
 719            return;
 720        }
 721
 722        self.editor.update(cx, |editor, cx| {
 723            editor.clear_inlay_hints(cx);
 724        });
 725
 726        cx.emit(MessageEditorEvent::SendImmediately)
 727    }
 728
 729    fn chat_with_follow(
 730        &mut self,
 731        _: &ChatWithFollow,
 732        window: &mut Window,
 733        cx: &mut Context<Self>,
 734    ) {
 735        self.workspace
 736            .update(cx, |this, cx| {
 737                this.follow(CollaboratorId::Agent, window, cx)
 738            })
 739            .log_err();
 740
 741        self.send(cx);
 742    }
 743
 744    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 745        cx.emit(MessageEditorEvent::Cancel)
 746    }
 747
 748    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 749        let Some(workspace) = self.workspace.upgrade() else {
 750            return;
 751        };
 752        let editor_clipboard_selections = cx
 753            .read_from_clipboard()
 754            .and_then(|item| item.entries().first().cloned())
 755            .and_then(|entry| match entry {
 756                ClipboardEntry::String(text) => {
 757                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
 758                }
 759                _ => None,
 760            });
 761
 762        // Insert creases for pasted clipboard selections that:
 763        // 1. Contain exactly one selection
 764        // 2. Have an associated file path
 765        // 3. Span multiple lines (not single-line selections)
 766        // 4. Belong to a file that exists in the current project
 767        let should_insert_creases = util::maybe!({
 768            let selections = editor_clipboard_selections.as_ref()?;
 769            if selections.len() > 1 {
 770                return Some(false);
 771            }
 772            let selection = selections.first()?;
 773            let file_path = selection.file_path.as_ref()?;
 774            let line_range = selection.line_range.as_ref()?;
 775
 776            if line_range.start() == line_range.end() {
 777                return Some(false);
 778            }
 779
 780            Some(
 781                workspace
 782                    .read(cx)
 783                    .project()
 784                    .read(cx)
 785                    .project_path_for_absolute_path(file_path, cx)
 786                    .is_some(),
 787            )
 788        })
 789        .unwrap_or(false);
 790
 791        if should_insert_creases && let Some(selections) = editor_clipboard_selections {
 792            cx.stop_propagation();
 793            let insertion_target = self
 794                .editor
 795                .read(cx)
 796                .selections
 797                .newest_anchor()
 798                .start
 799                .text_anchor;
 800
 801            let project = workspace.read(cx).project().clone();
 802            for selection in selections {
 803                if let (Some(file_path), Some(line_range)) =
 804                    (selection.file_path, selection.line_range)
 805                {
 806                    let crease_text =
 807                        acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
 808
 809                    let mention_uri = MentionUri::Selection {
 810                        abs_path: Some(file_path.clone()),
 811                        line_range: line_range.clone(),
 812                    };
 813
 814                    let mention_text = mention_uri.as_link().to_string();
 815                    let (excerpt_id, text_anchor, content_len) =
 816                        self.editor.update(cx, |editor, cx| {
 817                            let buffer = editor.buffer().read(cx);
 818                            let snapshot = buffer.snapshot(cx);
 819                            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
 820                            let text_anchor = insertion_target.bias_left(&buffer_snapshot);
 821
 822                            editor.insert(&mention_text, window, cx);
 823                            editor.insert(" ", window, cx);
 824
 825                            (*excerpt_id, text_anchor, mention_text.len())
 826                        });
 827
 828                    let Some((crease_id, tx)) = insert_crease_for_mention(
 829                        excerpt_id,
 830                        text_anchor,
 831                        content_len,
 832                        crease_text.into(),
 833                        mention_uri.icon_path(cx),
 834                        None,
 835                        self.editor.clone(),
 836                        window,
 837                        cx,
 838                    ) else {
 839                        continue;
 840                    };
 841                    drop(tx);
 842
 843                    let mention_task = cx
 844                        .spawn({
 845                            let project = project.clone();
 846                            async move |_, cx| {
 847                                let project_path = project
 848                                    .update(cx, |project, cx| {
 849                                        project.project_path_for_absolute_path(&file_path, cx)
 850                                    })
 851                                    .ok_or_else(|| "project path not found".to_string())?;
 852
 853                                let buffer = project
 854                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
 855                                    .await
 856                                    .map_err(|e| e.to_string())?;
 857
 858                                Ok(buffer.update(cx, |buffer, cx| {
 859                                    let start =
 860                                        Point::new(*line_range.start(), 0).min(buffer.max_point());
 861                                    let end = Point::new(*line_range.end() + 1, 0)
 862                                        .min(buffer.max_point());
 863                                    let content = buffer.text_for_range(start..end).collect();
 864                                    Mention::Text {
 865                                        content,
 866                                        tracked_buffers: vec![cx.entity()],
 867                                    }
 868                                }))
 869                            }
 870                        })
 871                        .shared();
 872
 873                    self.mention_set.update(cx, |mention_set, _cx| {
 874                        mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
 875                    });
 876                }
 877            }
 878            return;
 879        }
 880
 881        if self.prompt_capabilities.borrow().image
 882            && let Some(task) =
 883                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
 884        {
 885            task.detach();
 886        }
 887    }
 888
 889    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
 890        let editor = self.editor.clone();
 891        window.defer(cx, move |window, cx| {
 892            editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
 893        });
 894    }
 895
 896    pub fn insert_dragged_files(
 897        &mut self,
 898        paths: Vec<project::ProjectPath>,
 899        added_worktrees: Vec<Entity<Worktree>>,
 900        window: &mut Window,
 901        cx: &mut Context<Self>,
 902    ) {
 903        let Some(workspace) = self.workspace.upgrade() else {
 904            return;
 905        };
 906        let project = workspace.read(cx).project().clone();
 907        let path_style = project.read(cx).path_style(cx);
 908        let buffer = self.editor.read(cx).buffer().clone();
 909        let Some(buffer) = buffer.read(cx).as_singleton() else {
 910            return;
 911        };
 912        let mut tasks = Vec::new();
 913        for path in paths {
 914            let Some(entry) = project.read(cx).entry_for_path(&path, cx) else {
 915                continue;
 916            };
 917            let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 918                continue;
 919            };
 920            let abs_path = worktree.read(cx).absolutize(&path.path);
 921            let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 922                &path.path,
 923                worktree.read(cx).root_name(),
 924                path_style,
 925            );
 926
 927            let uri = if entry.is_dir() {
 928                MentionUri::Directory { abs_path }
 929            } else {
 930                MentionUri::File { abs_path }
 931            };
 932
 933            let new_text = format!("{} ", uri.as_link());
 934            let content_len = new_text.len() - 1;
 935
 936            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 937
 938            self.editor.update(cx, |message_editor, cx| {
 939                message_editor.edit(
 940                    [(
 941                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 942                        new_text,
 943                    )],
 944                    cx,
 945                );
 946            });
 947            let supports_images = self.prompt_capabilities.borrow().image;
 948            tasks.push(self.mention_set.update(cx, |mention_set, cx| {
 949                mention_set.confirm_mention_completion(
 950                    file_name,
 951                    anchor,
 952                    content_len,
 953                    uri,
 954                    supports_images,
 955                    self.editor.clone(),
 956                    &workspace,
 957                    window,
 958                    cx,
 959                )
 960            }));
 961        }
 962        cx.spawn(async move |_, _| {
 963            join_all(tasks).await;
 964            drop(added_worktrees);
 965        })
 966        .detach();
 967    }
 968
 969    /// Inserts code snippets as creases into the editor.
 970    /// Each tuple contains (code_text, crease_title).
 971    pub fn insert_code_creases(
 972        &mut self,
 973        creases: Vec<(String, String)>,
 974        window: &mut Window,
 975        cx: &mut Context<Self>,
 976    ) {
 977        use editor::display_map::{Crease, FoldPlaceholder};
 978        use multi_buffer::MultiBufferRow;
 979        use rope::Point;
 980
 981        self.editor.update(cx, |editor, cx| {
 982            editor.insert("\n", window, cx);
 983            for (text, crease_title) in creases {
 984                let point = editor
 985                    .selections
 986                    .newest::<Point>(&editor.display_snapshot(cx))
 987                    .head();
 988                let start_row = MultiBufferRow(point.row);
 989
 990                editor.insert(&text, window, cx);
 991
 992                let snapshot = editor.buffer().read(cx).snapshot(cx);
 993                let anchor_before = snapshot.anchor_after(point);
 994                let anchor_after = editor
 995                    .selections
 996                    .newest_anchor()
 997                    .head()
 998                    .bias_left(&snapshot);
 999
1000                editor.insert("\n", window, cx);
1001
1002                let fold_placeholder = FoldPlaceholder {
1003                    render: Arc::new({
1004                        let title = crease_title.clone();
1005                        move |_fold_id, _fold_range, _cx| {
1006                            ButtonLike::new("code-crease")
1007                                .style(ButtonStyle::Filled)
1008                                .layer(ElevationIndex::ElevatedSurface)
1009                                .child(Icon::new(IconName::TextSnippet))
1010                                .child(Label::new(title.clone()).single_line())
1011                                .into_any_element()
1012                        }
1013                    }),
1014                    merge_adjacent: false,
1015                    ..Default::default()
1016                };
1017
1018                let crease = Crease::inline(
1019                    anchor_before..anchor_after,
1020                    fold_placeholder,
1021                    |row, is_folded, fold, _window, _cx| {
1022                        Disclosure::new(("code-crease-toggle", row.0 as u64), !is_folded)
1023                            .toggle_state(is_folded)
1024                            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
1025                            .into_any_element()
1026                    },
1027                    |_, _, _, _| gpui::Empty.into_any(),
1028                );
1029                editor.insert_creases(vec![crease], cx);
1030                editor.fold_at(start_row, window, cx);
1031            }
1032        });
1033    }
1034
1035    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1036        let editor = self.editor.read(cx);
1037        let editor_buffer = editor.buffer().read(cx);
1038        let Some(buffer) = editor_buffer.as_singleton() else {
1039            return;
1040        };
1041        let cursor_anchor = editor.selections.newest_anchor().head();
1042        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1043        let anchor = buffer.update(cx, |buffer, _cx| {
1044            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
1045        });
1046        let Some(workspace) = self.workspace.upgrade() else {
1047            return;
1048        };
1049        let Some(completion) =
1050            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
1051                PromptContextAction::AddSelections,
1052                anchor..anchor,
1053                self.editor.downgrade(),
1054                self.mention_set.downgrade(),
1055                &workspace,
1056                cx,
1057            )
1058        else {
1059            return;
1060        };
1061
1062        self.editor.update(cx, |message_editor, cx| {
1063            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1064            message_editor.request_autoscroll(Autoscroll::fit(), cx);
1065        });
1066        if let Some(confirm) = completion.confirm {
1067            confirm(CompletionIntent::Complete, window, cx);
1068        }
1069    }
1070
1071    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1072        self.editor.update(cx, |message_editor, cx| {
1073            message_editor.set_read_only(read_only);
1074            cx.notify()
1075        })
1076    }
1077
1078    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1079        self.editor.update(cx, |editor, cx| {
1080            editor.set_mode(mode);
1081            cx.notify()
1082        });
1083    }
1084
1085    pub fn set_message(
1086        &mut self,
1087        message: Vec<acp::ContentBlock>,
1088        window: &mut Window,
1089        cx: &mut Context<Self>,
1090    ) {
1091        let Some(workspace) = self.workspace.upgrade() else {
1092            return;
1093        };
1094
1095        self.clear(window, cx);
1096
1097        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1098        let mut text = String::new();
1099        let mut mentions = Vec::new();
1100
1101        for chunk in message {
1102            match chunk {
1103                acp::ContentBlock::Text(text_content) => {
1104                    text.push_str(&text_content.text);
1105                }
1106                acp::ContentBlock::Resource(acp::EmbeddedResource {
1107                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1108                    ..
1109                }) => {
1110                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1111                    else {
1112                        continue;
1113                    };
1114                    let start = text.len();
1115                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1116                    let end = text.len();
1117                    mentions.push((
1118                        start..end,
1119                        mention_uri,
1120                        Mention::Text {
1121                            content: resource.text,
1122                            tracked_buffers: Vec::new(),
1123                        },
1124                    ));
1125                }
1126                acp::ContentBlock::ResourceLink(resource) => {
1127                    if let Some(mention_uri) =
1128                        MentionUri::parse(&resource.uri, path_style).log_err()
1129                    {
1130                        let start = text.len();
1131                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1132                        let end = text.len();
1133                        mentions.push((start..end, mention_uri, Mention::Link));
1134                    }
1135                }
1136                acp::ContentBlock::Image(acp::ImageContent {
1137                    uri,
1138                    data,
1139                    mime_type,
1140                    ..
1141                }) => {
1142                    let mention_uri = if let Some(uri) = uri {
1143                        MentionUri::parse(&uri, path_style)
1144                    } else {
1145                        Ok(MentionUri::PastedImage)
1146                    };
1147                    let Some(mention_uri) = mention_uri.log_err() else {
1148                        continue;
1149                    };
1150                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1151                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1152                        continue;
1153                    };
1154                    let start = text.len();
1155                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1156                    let end = text.len();
1157                    mentions.push((
1158                        start..end,
1159                        mention_uri,
1160                        Mention::Image(MentionImage {
1161                            data: data.into(),
1162                            format,
1163                        }),
1164                    ));
1165                }
1166                _ => {}
1167            }
1168        }
1169
1170        let snapshot = self.editor.update(cx, |editor, cx| {
1171            editor.set_text(text, window, cx);
1172            editor.buffer().read(cx).snapshot(cx)
1173        });
1174
1175        for (range, mention_uri, mention) in mentions {
1176            let anchor = snapshot.anchor_before(MultiBufferOffset(range.start));
1177            let Some((crease_id, tx)) = insert_crease_for_mention(
1178                anchor.excerpt_id,
1179                anchor.text_anchor,
1180                range.end - range.start,
1181                mention_uri.name().into(),
1182                mention_uri.icon_path(cx),
1183                None,
1184                self.editor.clone(),
1185                window,
1186                cx,
1187            ) else {
1188                continue;
1189            };
1190            drop(tx);
1191
1192            self.mention_set.update(cx, |mention_set, _cx| {
1193                mention_set.insert_mention(
1194                    crease_id,
1195                    mention_uri.clone(),
1196                    Task::ready(Ok(mention)).shared(),
1197                )
1198            });
1199        }
1200        cx.notify();
1201    }
1202
1203    pub fn text(&self, cx: &App) -> String {
1204        self.editor.read(cx).text(cx)
1205    }
1206
1207    pub fn set_placeholder_text(
1208        &mut self,
1209        placeholder: &str,
1210        window: &mut Window,
1211        cx: &mut Context<Self>,
1212    ) {
1213        self.editor.update(cx, |editor, cx| {
1214            editor.set_placeholder_text(placeholder, window, cx);
1215        });
1216    }
1217
1218    #[cfg(test)]
1219    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1220        self.editor.update(cx, |editor, cx| {
1221            editor.set_text(text, window, cx);
1222        });
1223    }
1224}
1225
1226impl Focusable for MessageEditor {
1227    fn focus_handle(&self, cx: &App) -> FocusHandle {
1228        self.editor.focus_handle(cx)
1229    }
1230}
1231
1232impl Render for MessageEditor {
1233    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1234        div()
1235            .key_context("MessageEditor")
1236            .on_action(cx.listener(Self::chat))
1237            .on_action(cx.listener(Self::send_immediately))
1238            .on_action(cx.listener(Self::chat_with_follow))
1239            .on_action(cx.listener(Self::cancel))
1240            .on_action(cx.listener(Self::paste_raw))
1241            .capture_action(cx.listener(Self::paste))
1242            .flex_1()
1243            .child({
1244                let settings = ThemeSettings::get_global(cx);
1245
1246                let text_style = TextStyle {
1247                    color: cx.theme().colors().text,
1248                    font_family: settings.buffer_font.family.clone(),
1249                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1250                    font_features: settings.buffer_font.features.clone(),
1251                    font_size: settings.agent_buffer_font_size(cx).into(),
1252                    line_height: relative(settings.buffer_line_height.value()),
1253                    ..Default::default()
1254                };
1255
1256                EditorElement::new(
1257                    &self.editor,
1258                    EditorStyle {
1259                        background: cx.theme().colors().editor_background,
1260                        local_player: cx.theme().players().local(),
1261                        text: text_style,
1262                        syntax: cx.theme().syntax().clone(),
1263                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1264                        ..Default::default()
1265                    },
1266                )
1267            })
1268    }
1269}
1270
1271pub struct MessageEditorAddon {}
1272
1273impl MessageEditorAddon {
1274    pub fn new() -> Self {
1275        Self {}
1276    }
1277}
1278
1279impl Addon for MessageEditorAddon {
1280    fn to_any(&self) -> &dyn std::any::Any {
1281        self
1282    }
1283
1284    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1285        Some(self)
1286    }
1287
1288    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1289        let settings = agent_settings::AgentSettings::get_global(cx);
1290        if settings.use_modifier_to_send {
1291            key_context.add("use_modifier_to_send");
1292        }
1293    }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1299
1300    use acp_thread::{AgentSessionInfo, MentionUri};
1301    use agent::{ThreadStore, outline};
1302    use agent_client_protocol as acp;
1303    use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset};
1304
1305    use fs::FakeFs;
1306    use futures::StreamExt as _;
1307    use gpui::{
1308        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1309    };
1310    use language_model::LanguageModelRegistry;
1311    use lsp::{CompletionContext, CompletionTriggerKind};
1312    use project::{CompletionIntent, Project, ProjectPath};
1313    use serde_json::json;
1314
1315    use text::Point;
1316    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1317    use util::{path, paths::PathStyle, rel_path::rel_path};
1318    use workspace::{AppState, Item, Workspace};
1319
1320    use crate::acp::{
1321        message_editor::{Mention, MessageEditor},
1322        thread_view::tests::init_test,
1323    };
1324    use crate::completion_provider::{PromptCompletionProviderDelegate, PromptContextType};
1325
1326    #[gpui::test]
1327    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1328        init_test(cx);
1329
1330        let fs = FakeFs::new(cx.executor());
1331        fs.insert_tree("/project", json!({"file": ""})).await;
1332        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1333
1334        let (workspace, cx) =
1335            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1336
1337        let thread_store = None;
1338        let history = cx
1339            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1340
1341        let message_editor = cx.update(|window, cx| {
1342            cx.new(|cx| {
1343                MessageEditor::new_with_cache(
1344                    workspace.downgrade(),
1345                    project.downgrade(),
1346                    thread_store.clone(),
1347                    history.downgrade(),
1348                    None,
1349                    Default::default(),
1350                    Default::default(),
1351                    Default::default(),
1352                    Default::default(),
1353                    "Test Agent".into(),
1354                    "Test",
1355                    EditorMode::AutoHeight {
1356                        min_lines: 1,
1357                        max_lines: None,
1358                    },
1359                    window,
1360                    cx,
1361                )
1362            })
1363        });
1364        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1365
1366        cx.run_until_parked();
1367
1368        let excerpt_id = editor.update(cx, |editor, cx| {
1369            editor
1370                .buffer()
1371                .read(cx)
1372                .excerpt_ids()
1373                .into_iter()
1374                .next()
1375                .unwrap()
1376        });
1377        let completions = editor.update_in(cx, |editor, window, cx| {
1378            editor.set_text("Hello @file ", window, cx);
1379            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1380            let completion_provider = editor.completion_provider().unwrap();
1381            completion_provider.completions(
1382                excerpt_id,
1383                &buffer,
1384                text::Anchor::MAX,
1385                CompletionContext {
1386                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1387                    trigger_character: Some("@".into()),
1388                },
1389                window,
1390                cx,
1391            )
1392        });
1393        let [_, completion]: [_; 2] = completions
1394            .await
1395            .unwrap()
1396            .into_iter()
1397            .flat_map(|response| response.completions)
1398            .collect::<Vec<_>>()
1399            .try_into()
1400            .unwrap();
1401
1402        editor.update_in(cx, |editor, window, cx| {
1403            let snapshot = editor.buffer().read(cx).snapshot(cx);
1404            let range = snapshot
1405                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1406                .unwrap();
1407            editor.edit([(range, completion.new_text)], cx);
1408            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1409        });
1410
1411        cx.run_until_parked();
1412
1413        // Backspace over the inserted crease (and the following space).
1414        editor.update_in(cx, |editor, window, cx| {
1415            editor.backspace(&Default::default(), window, cx);
1416            editor.backspace(&Default::default(), window, cx);
1417        });
1418
1419        let (content, _) = message_editor
1420            .update(cx, |message_editor, cx| {
1421                message_editor.contents_with_cache(false, None, None, cx)
1422            })
1423            .await
1424            .unwrap();
1425
1426        // We don't send a resource link for the deleted crease.
1427        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1428    }
1429
1430    #[gpui::test]
1431    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1432        init_test(cx);
1433        let fs = FakeFs::new(cx.executor());
1434        fs.insert_tree(
1435            "/test",
1436            json!({
1437                ".zed": {
1438                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1439                },
1440                "src": {
1441                    "main.rs": "fn main() {}",
1442                },
1443            }),
1444        )
1445        .await;
1446
1447        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1448        let thread_store = None;
1449        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1450        // Start with no available commands - simulating Claude which doesn't support slash commands
1451        let available_commands = Rc::new(RefCell::new(vec![]));
1452
1453        let (workspace, cx) =
1454            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1455        let history = cx
1456            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1457        let workspace_handle = workspace.downgrade();
1458        let message_editor = workspace.update_in(cx, |_, window, cx| {
1459            cx.new(|cx| {
1460                MessageEditor::new_with_cache(
1461                    workspace_handle.clone(),
1462                    project.downgrade(),
1463                    thread_store.clone(),
1464                    history.downgrade(),
1465                    None,
1466                    prompt_capabilities.clone(),
1467                    available_commands.clone(),
1468                    Default::default(),
1469                    Default::default(),
1470                    "Claude Code".into(),
1471                    "Test",
1472                    EditorMode::AutoHeight {
1473                        min_lines: 1,
1474                        max_lines: None,
1475                    },
1476                    window,
1477                    cx,
1478                )
1479            })
1480        });
1481        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1482
1483        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1484        editor.update_in(cx, |editor, window, cx| {
1485            editor.set_text("/file test.txt", window, cx);
1486        });
1487
1488        let contents_result = message_editor
1489            .update(cx, |message_editor, cx| {
1490                message_editor.contents_with_cache(false, None, None, cx)
1491            })
1492            .await;
1493
1494        // Should fail because available_commands is empty (no commands supported)
1495        assert!(contents_result.is_err());
1496        let error_message = contents_result.unwrap_err().to_string();
1497        assert!(error_message.contains("not supported by Claude Code"));
1498        assert!(error_message.contains("Available commands: none"));
1499
1500        // Now simulate Claude providing its list of available commands (which doesn't include file)
1501        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
1502
1503        // Test that unsupported slash commands trigger an error when we have a list of available commands
1504        editor.update_in(cx, |editor, window, cx| {
1505            editor.set_text("/file test.txt", window, cx);
1506        });
1507
1508        let contents_result = message_editor
1509            .update(cx, |message_editor, cx| {
1510                message_editor.contents_with_cache(false, None, None, cx)
1511            })
1512            .await;
1513
1514        assert!(contents_result.is_err());
1515        let error_message = contents_result.unwrap_err().to_string();
1516        assert!(error_message.contains("not supported by Claude Code"));
1517        assert!(error_message.contains("/file"));
1518        assert!(error_message.contains("Available commands: /help"));
1519
1520        // Test that supported commands work fine
1521        editor.update_in(cx, |editor, window, cx| {
1522            editor.set_text("/help", window, cx);
1523        });
1524
1525        let contents_result = message_editor
1526            .update(cx, |message_editor, cx| {
1527                message_editor.contents_with_cache(false, None, None, cx)
1528            })
1529            .await;
1530
1531        // Should succeed because /help is in available_commands
1532        assert!(contents_result.is_ok());
1533
1534        // Test that regular text works fine
1535        editor.update_in(cx, |editor, window, cx| {
1536            editor.set_text("Hello Claude!", window, cx);
1537        });
1538
1539        let (content, _) = message_editor
1540            .update(cx, |message_editor, cx| {
1541                message_editor.contents_with_cache(false, None, None, cx)
1542            })
1543            .await
1544            .unwrap();
1545
1546        assert_eq!(content.len(), 1);
1547        if let acp::ContentBlock::Text(text) = &content[0] {
1548            assert_eq!(text.text, "Hello Claude!");
1549        } else {
1550            panic!("Expected ContentBlock::Text");
1551        }
1552
1553        // Test that @ mentions still work
1554        editor.update_in(cx, |editor, window, cx| {
1555            editor.set_text("Check this @", window, cx);
1556        });
1557
1558        // The @ mention functionality should not be affected
1559        let (content, _) = message_editor
1560            .update(cx, |message_editor, cx| {
1561                message_editor.contents_with_cache(false, None, None, cx)
1562            })
1563            .await
1564            .unwrap();
1565
1566        assert_eq!(content.len(), 1);
1567        if let acp::ContentBlock::Text(text) = &content[0] {
1568            assert_eq!(text.text, "Check this @");
1569        } else {
1570            panic!("Expected ContentBlock::Text");
1571        }
1572    }
1573
1574    struct MessageEditorItem(Entity<MessageEditor>);
1575
1576    impl Item for MessageEditorItem {
1577        type Event = ();
1578
1579        fn include_in_nav_history() -> bool {
1580            false
1581        }
1582
1583        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1584            "Test".into()
1585        }
1586    }
1587
1588    impl EventEmitter<()> for MessageEditorItem {}
1589
1590    impl Focusable for MessageEditorItem {
1591        fn focus_handle(&self, cx: &App) -> FocusHandle {
1592            self.0.read(cx).focus_handle(cx)
1593        }
1594    }
1595
1596    impl Render for MessageEditorItem {
1597        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1598            self.0.clone().into_any_element()
1599        }
1600    }
1601
1602    #[gpui::test]
1603    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1604        init_test(cx);
1605
1606        let app_state = cx.update(AppState::test);
1607
1608        cx.update(|cx| {
1609            editor::init(cx);
1610            workspace::init(app_state.clone(), cx);
1611        });
1612
1613        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1614        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1615        let workspace = window.root(cx).unwrap();
1616
1617        let mut cx = VisualTestContext::from_window(*window, cx);
1618
1619        let thread_store = None;
1620        let history = cx
1621            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1622        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1623        let available_commands = Rc::new(RefCell::new(vec![
1624            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
1625            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
1626                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
1627                    "<name>",
1628                )),
1629            ),
1630        ]));
1631
1632        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1633            let workspace_handle = cx.weak_entity();
1634            let message_editor = cx.new(|cx| {
1635                MessageEditor::new_with_cache(
1636                    workspace_handle,
1637                    project.downgrade(),
1638                    thread_store.clone(),
1639                    history.downgrade(),
1640                    None,
1641                    prompt_capabilities.clone(),
1642                    available_commands.clone(),
1643                    Default::default(),
1644                    Default::default(),
1645                    "Test Agent".into(),
1646                    "Test",
1647                    EditorMode::AutoHeight {
1648                        max_lines: None,
1649                        min_lines: 1,
1650                    },
1651                    window,
1652                    cx,
1653                )
1654            });
1655            workspace.active_pane().update(cx, |pane, cx| {
1656                pane.add_item(
1657                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1658                    true,
1659                    true,
1660                    None,
1661                    window,
1662                    cx,
1663                );
1664            });
1665            message_editor.read(cx).focus_handle(cx).focus(window, cx);
1666            message_editor.read(cx).editor().clone()
1667        });
1668
1669        cx.simulate_input("/");
1670
1671        editor.update_in(&mut cx, |editor, window, cx| {
1672            assert_eq!(editor.text(cx), "/");
1673            assert!(editor.has_visible_completions_menu());
1674
1675            assert_eq!(
1676                current_completion_labels_with_documentation(editor),
1677                &[
1678                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1679                    ("say-hello".into(), "Say hello to whoever you want".into())
1680                ]
1681            );
1682            editor.set_text("", window, cx);
1683        });
1684
1685        cx.simulate_input("/qui");
1686
1687        editor.update_in(&mut cx, |editor, window, cx| {
1688            assert_eq!(editor.text(cx), "/qui");
1689            assert!(editor.has_visible_completions_menu());
1690
1691            assert_eq!(
1692                current_completion_labels_with_documentation(editor),
1693                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1694            );
1695            editor.set_text("", window, cx);
1696        });
1697
1698        editor.update_in(&mut cx, |editor, window, cx| {
1699            assert!(editor.has_visible_completions_menu());
1700            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1701        });
1702
1703        cx.run_until_parked();
1704
1705        editor.update_in(&mut cx, |editor, window, cx| {
1706            assert_eq!(editor.display_text(cx), "/quick-math ");
1707            assert!(!editor.has_visible_completions_menu());
1708            editor.set_text("", window, cx);
1709        });
1710
1711        cx.simulate_input("/say");
1712
1713        editor.update_in(&mut cx, |editor, _window, cx| {
1714            assert_eq!(editor.display_text(cx), "/say");
1715            assert!(editor.has_visible_completions_menu());
1716
1717            assert_eq!(
1718                current_completion_labels_with_documentation(editor),
1719                &[("say-hello".into(), "Say hello to whoever you want".into())]
1720            );
1721        });
1722
1723        editor.update_in(&mut cx, |editor, window, cx| {
1724            assert!(editor.has_visible_completions_menu());
1725            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1726        });
1727
1728        cx.run_until_parked();
1729
1730        editor.update_in(&mut cx, |editor, _window, cx| {
1731            assert_eq!(editor.text(cx), "/say-hello ");
1732            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1733            assert!(!editor.has_visible_completions_menu());
1734        });
1735
1736        cx.simulate_input("GPT5");
1737
1738        cx.run_until_parked();
1739
1740        editor.update_in(&mut cx, |editor, window, cx| {
1741            assert_eq!(editor.text(cx), "/say-hello GPT5");
1742            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
1743            assert!(!editor.has_visible_completions_menu());
1744
1745            // Delete argument
1746            for _ in 0..5 {
1747                editor.backspace(&editor::actions::Backspace, window, cx);
1748            }
1749        });
1750
1751        cx.run_until_parked();
1752
1753        editor.update_in(&mut cx, |editor, window, cx| {
1754            assert_eq!(editor.text(cx), "/say-hello");
1755            // Hint is visible because argument was deleted
1756            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1757
1758            // Delete last command letter
1759            editor.backspace(&editor::actions::Backspace, window, cx);
1760        });
1761
1762        cx.run_until_parked();
1763
1764        editor.update_in(&mut cx, |editor, _window, cx| {
1765            // Hint goes away once command no longer matches an available one
1766            assert_eq!(editor.text(cx), "/say-hell");
1767            assert_eq!(editor.display_text(cx), "/say-hell");
1768            assert!(!editor.has_visible_completions_menu());
1769        });
1770    }
1771
1772    #[gpui::test]
1773    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
1774        init_test(cx);
1775
1776        let app_state = cx.update(AppState::test);
1777
1778        cx.update(|cx| {
1779            editor::init(cx);
1780            workspace::init(app_state.clone(), cx);
1781        });
1782
1783        app_state
1784            .fs
1785            .as_fake()
1786            .insert_tree(
1787                path!("/dir"),
1788                json!({
1789                    "editor": "",
1790                    "a": {
1791                        "one.txt": "1",
1792                        "two.txt": "2",
1793                        "three.txt": "3",
1794                        "four.txt": "4"
1795                    },
1796                    "b": {
1797                        "five.txt": "5",
1798                        "six.txt": "6",
1799                        "seven.txt": "7",
1800                        "eight.txt": "8",
1801                    },
1802                    "x.png": "",
1803                }),
1804            )
1805            .await;
1806
1807        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1808        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1809        let workspace = window.root(cx).unwrap();
1810
1811        let worktree = project.update(cx, |project, cx| {
1812            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1813            assert_eq!(worktrees.len(), 1);
1814            worktrees.pop().unwrap()
1815        });
1816        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1817
1818        let mut cx = VisualTestContext::from_window(*window, cx);
1819
1820        let paths = vec![
1821            rel_path("a/one.txt"),
1822            rel_path("a/two.txt"),
1823            rel_path("a/three.txt"),
1824            rel_path("a/four.txt"),
1825            rel_path("b/five.txt"),
1826            rel_path("b/six.txt"),
1827            rel_path("b/seven.txt"),
1828            rel_path("b/eight.txt"),
1829        ];
1830
1831        let slash = PathStyle::local().primary_separator();
1832
1833        let mut opened_editors = Vec::new();
1834        for path in paths {
1835            let buffer = workspace
1836                .update_in(&mut cx, |workspace, window, cx| {
1837                    workspace.open_path(
1838                        ProjectPath {
1839                            worktree_id,
1840                            path: path.into(),
1841                        },
1842                        None,
1843                        false,
1844                        window,
1845                        cx,
1846                    )
1847                })
1848                .await
1849                .unwrap();
1850            opened_editors.push(buffer);
1851        }
1852
1853        let thread_store = cx.new(|cx| ThreadStore::new(cx));
1854        let history = cx
1855            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1856        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1857
1858        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1859            let workspace_handle = cx.weak_entity();
1860            let message_editor = cx.new(|cx| {
1861                MessageEditor::new_with_cache(
1862                    workspace_handle,
1863                    project.downgrade(),
1864                    Some(thread_store),
1865                    history.downgrade(),
1866                    None,
1867                    prompt_capabilities.clone(),
1868                    Default::default(),
1869                    Default::default(),
1870                    Default::default(),
1871                    "Test Agent".into(),
1872                    "Test",
1873                    EditorMode::AutoHeight {
1874                        max_lines: None,
1875                        min_lines: 1,
1876                    },
1877                    window,
1878                    cx,
1879                )
1880            });
1881            workspace.active_pane().update(cx, |pane, cx| {
1882                pane.add_item(
1883                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1884                    true,
1885                    true,
1886                    None,
1887                    window,
1888                    cx,
1889                );
1890            });
1891            message_editor.read(cx).focus_handle(cx).focus(window, cx);
1892            let editor = message_editor.read(cx).editor().clone();
1893            (message_editor, editor)
1894        });
1895
1896        cx.simulate_input("Lorem @");
1897
1898        editor.update_in(&mut cx, |editor, window, cx| {
1899            assert_eq!(editor.text(cx), "Lorem @");
1900            assert!(editor.has_visible_completions_menu());
1901
1902            assert_eq!(
1903                current_completion_labels(editor),
1904                &[
1905                    format!("eight.txt b{slash}"),
1906                    format!("seven.txt b{slash}"),
1907                    format!("six.txt b{slash}"),
1908                    format!("five.txt b{slash}"),
1909                    "Files & Directories".into(),
1910                    "Symbols".into()
1911                ]
1912            );
1913            editor.set_text("", window, cx);
1914        });
1915
1916        prompt_capabilities.replace(
1917            acp::PromptCapabilities::new()
1918                .image(true)
1919                .audio(true)
1920                .embedded_context(true),
1921        );
1922
1923        cx.simulate_input("Lorem ");
1924
1925        editor.update(&mut cx, |editor, cx| {
1926            assert_eq!(editor.text(cx), "Lorem ");
1927            assert!(!editor.has_visible_completions_menu());
1928        });
1929
1930        cx.simulate_input("@");
1931
1932        editor.update(&mut cx, |editor, cx| {
1933            assert_eq!(editor.text(cx), "Lorem @");
1934            assert!(editor.has_visible_completions_menu());
1935            assert_eq!(
1936                current_completion_labels(editor),
1937                &[
1938                    format!("eight.txt b{slash}"),
1939                    format!("seven.txt b{slash}"),
1940                    format!("six.txt b{slash}"),
1941                    format!("five.txt b{slash}"),
1942                    "Files & Directories".into(),
1943                    "Symbols".into(),
1944                    "Threads".into(),
1945                    "Fetch".into()
1946                ]
1947            );
1948        });
1949
1950        // Select and confirm "File"
1951        editor.update_in(&mut cx, |editor, window, cx| {
1952            assert!(editor.has_visible_completions_menu());
1953            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1954            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1955            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1956            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1957            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1958        });
1959
1960        cx.run_until_parked();
1961
1962        editor.update(&mut cx, |editor, cx| {
1963            assert_eq!(editor.text(cx), "Lorem @file ");
1964            assert!(editor.has_visible_completions_menu());
1965        });
1966
1967        cx.simulate_input("one");
1968
1969        editor.update(&mut cx, |editor, cx| {
1970            assert_eq!(editor.text(cx), "Lorem @file one");
1971            assert!(editor.has_visible_completions_menu());
1972            assert_eq!(
1973                current_completion_labels(editor),
1974                vec![format!("one.txt a{slash}")]
1975            );
1976        });
1977
1978        editor.update_in(&mut cx, |editor, window, cx| {
1979            assert!(editor.has_visible_completions_menu());
1980            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1981        });
1982
1983        let url_one = MentionUri::File {
1984            abs_path: path!("/dir/a/one.txt").into(),
1985        }
1986        .to_uri()
1987        .to_string();
1988        editor.update(&mut cx, |editor, cx| {
1989            let text = editor.text(cx);
1990            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
1991            assert!(!editor.has_visible_completions_menu());
1992            assert_eq!(fold_ranges(editor, cx).len(), 1);
1993        });
1994
1995        let contents = message_editor
1996            .update(&mut cx, |message_editor, cx| {
1997                message_editor
1998                    .mention_set()
1999                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2000            })
2001            .await
2002            .unwrap()
2003            .into_values()
2004            .collect::<Vec<_>>();
2005
2006        {
2007            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2008                panic!("Unexpected mentions");
2009            };
2010            pretty_assertions::assert_eq!(content, "1");
2011            pretty_assertions::assert_eq!(
2012                uri,
2013                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2014            );
2015        }
2016
2017        cx.simulate_input(" ");
2018
2019        editor.update(&mut cx, |editor, cx| {
2020            let text = editor.text(cx);
2021            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2022            assert!(!editor.has_visible_completions_menu());
2023            assert_eq!(fold_ranges(editor, cx).len(), 1);
2024        });
2025
2026        cx.simulate_input("Ipsum ");
2027
2028        editor.update(&mut cx, |editor, cx| {
2029            let text = editor.text(cx);
2030            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2031            assert!(!editor.has_visible_completions_menu());
2032            assert_eq!(fold_ranges(editor, cx).len(), 1);
2033        });
2034
2035        cx.simulate_input("@file ");
2036
2037        editor.update(&mut cx, |editor, cx| {
2038            let text = editor.text(cx);
2039            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2040            assert!(editor.has_visible_completions_menu());
2041            assert_eq!(fold_ranges(editor, cx).len(), 1);
2042        });
2043
2044        editor.update_in(&mut cx, |editor, window, cx| {
2045            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2046        });
2047
2048        cx.run_until_parked();
2049
2050        let contents = message_editor
2051            .update(&mut cx, |message_editor, cx| {
2052                message_editor
2053                    .mention_set()
2054                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2055            })
2056            .await
2057            .unwrap()
2058            .into_values()
2059            .collect::<Vec<_>>();
2060
2061        let url_eight = MentionUri::File {
2062            abs_path: path!("/dir/b/eight.txt").into(),
2063        }
2064        .to_uri()
2065        .to_string();
2066
2067        {
2068            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2069                panic!("Unexpected mentions");
2070            };
2071            pretty_assertions::assert_eq!(content, "8");
2072            pretty_assertions::assert_eq!(
2073                uri,
2074                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2075            );
2076        }
2077
2078        editor.update(&mut cx, |editor, cx| {
2079            assert_eq!(
2080                editor.text(cx),
2081                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2082            );
2083            assert!(!editor.has_visible_completions_menu());
2084            assert_eq!(fold_ranges(editor, cx).len(), 2);
2085        });
2086
2087        let plain_text_language = Arc::new(language::Language::new(
2088            language::LanguageConfig {
2089                name: "Plain Text".into(),
2090                matcher: language::LanguageMatcher {
2091                    path_suffixes: vec!["txt".to_string()],
2092                    ..Default::default()
2093                },
2094                ..Default::default()
2095            },
2096            None,
2097        ));
2098
2099        // Register the language and fake LSP
2100        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2101        language_registry.add(plain_text_language);
2102
2103        let mut fake_language_servers = language_registry.register_fake_lsp(
2104            "Plain Text",
2105            language::FakeLspAdapter {
2106                capabilities: lsp::ServerCapabilities {
2107                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2108                    ..Default::default()
2109                },
2110                ..Default::default()
2111            },
2112        );
2113
2114        // Open the buffer to trigger LSP initialization
2115        let buffer = project
2116            .update(&mut cx, |project, cx| {
2117                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2118            })
2119            .await
2120            .unwrap();
2121
2122        // Register the buffer with language servers
2123        let _handle = project.update(&mut cx, |project, cx| {
2124            project.register_buffer_with_language_servers(&buffer, cx)
2125        });
2126
2127        cx.run_until_parked();
2128
2129        let fake_language_server = fake_language_servers.next().await.unwrap();
2130        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2131            move |_, _| async move {
2132                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2133                    #[allow(deprecated)]
2134                    lsp::SymbolInformation {
2135                        name: "MySymbol".into(),
2136                        location: lsp::Location {
2137                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2138                            range: lsp::Range::new(
2139                                lsp::Position::new(0, 0),
2140                                lsp::Position::new(0, 1),
2141                            ),
2142                        },
2143                        kind: lsp::SymbolKind::CONSTANT,
2144                        tags: None,
2145                        container_name: None,
2146                        deprecated: None,
2147                    },
2148                ])))
2149            },
2150        );
2151
2152        cx.simulate_input("@symbol ");
2153
2154        editor.update(&mut cx, |editor, cx| {
2155            assert_eq!(
2156                editor.text(cx),
2157                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2158            );
2159            assert!(editor.has_visible_completions_menu());
2160            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2161        });
2162
2163        editor.update_in(&mut cx, |editor, window, cx| {
2164            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2165        });
2166
2167        let symbol = MentionUri::Symbol {
2168            abs_path: path!("/dir/a/one.txt").into(),
2169            name: "MySymbol".into(),
2170            line_range: 0..=0,
2171        };
2172
2173        let contents = message_editor
2174            .update(&mut cx, |message_editor, cx| {
2175                message_editor
2176                    .mention_set()
2177                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2178            })
2179            .await
2180            .unwrap()
2181            .into_values()
2182            .collect::<Vec<_>>();
2183
2184        {
2185            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2186                panic!("Unexpected mentions");
2187            };
2188            pretty_assertions::assert_eq!(content, "1");
2189            pretty_assertions::assert_eq!(uri, &symbol);
2190        }
2191
2192        cx.run_until_parked();
2193
2194        editor.read_with(&cx, |editor, cx| {
2195            assert_eq!(
2196                editor.text(cx),
2197                format!(
2198                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2199                    symbol.to_uri(),
2200                )
2201            );
2202        });
2203
2204        // Try to mention an "image" file that will fail to load
2205        cx.simulate_input("@file x.png");
2206
2207        editor.update(&mut cx, |editor, cx| {
2208            assert_eq!(
2209                editor.text(cx),
2210                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2211            );
2212            assert!(editor.has_visible_completions_menu());
2213            assert_eq!(current_completion_labels(editor), &["x.png "]);
2214        });
2215
2216        editor.update_in(&mut cx, |editor, window, cx| {
2217            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2218        });
2219
2220        // Getting the message contents fails
2221        message_editor
2222            .update(&mut cx, |message_editor, cx| {
2223                message_editor
2224                    .mention_set()
2225                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2226            })
2227            .await
2228            .expect_err("Should fail to load x.png");
2229
2230        cx.run_until_parked();
2231
2232        // Mention was removed
2233        editor.read_with(&cx, |editor, cx| {
2234            assert_eq!(
2235                editor.text(cx),
2236                format!(
2237                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2238                    symbol.to_uri()
2239                )
2240            );
2241        });
2242
2243        // Once more
2244        cx.simulate_input("@file x.png");
2245
2246        editor.update(&mut cx, |editor, cx| {
2247                    assert_eq!(
2248                        editor.text(cx),
2249                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2250                    );
2251                    assert!(editor.has_visible_completions_menu());
2252                    assert_eq!(current_completion_labels(editor), &["x.png "]);
2253                });
2254
2255        editor.update_in(&mut cx, |editor, window, cx| {
2256            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2257        });
2258
2259        // This time don't immediately get the contents, just let the confirmed completion settle
2260        cx.run_until_parked();
2261
2262        // Mention was removed
2263        editor.read_with(&cx, |editor, cx| {
2264            assert_eq!(
2265                editor.text(cx),
2266                format!(
2267                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2268                    symbol.to_uri()
2269                )
2270            );
2271        });
2272
2273        // Now getting the contents succeeds, because the invalid mention was removed
2274        let contents = message_editor
2275            .update(&mut cx, |message_editor, cx| {
2276                message_editor
2277                    .mention_set()
2278                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2279            })
2280            .await
2281            .unwrap();
2282        assert_eq!(contents.len(), 3);
2283    }
2284
2285    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2286        let snapshot = editor.buffer().read(cx).snapshot(cx);
2287        editor.display_map.update(cx, |display_map, cx| {
2288            display_map
2289                .snapshot(cx)
2290                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
2291                .map(|fold| fold.range.to_point(&snapshot))
2292                .collect()
2293        })
2294    }
2295
2296    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2297        let completions = editor.current_completions().expect("Missing completions");
2298        completions
2299            .into_iter()
2300            .map(|completion| completion.label.text)
2301            .collect::<Vec<_>>()
2302    }
2303
2304    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2305        let completions = editor.current_completions().expect("Missing completions");
2306        completions
2307            .into_iter()
2308            .map(|completion| {
2309                (
2310                    completion.label.text,
2311                    completion
2312                        .documentation
2313                        .map(|d| d.text().to_string())
2314                        .unwrap_or_default(),
2315                )
2316            })
2317            .collect::<Vec<_>>()
2318    }
2319
2320    #[gpui::test]
2321    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
2322        init_test(cx);
2323
2324        let fs = FakeFs::new(cx.executor());
2325
2326        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2327        // Using plain text without a configured language, so no outline is available
2328        const LINE: &str = "This is a line of text in the file\n";
2329        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2330        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2331
2332        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2333        let small_content = "fn small_function() { /* small */ }\n";
2334        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2335
2336        fs.insert_tree(
2337            "/project",
2338            json!({
2339                "large_file.txt": large_content.clone(),
2340                "small_file.txt": small_content,
2341            }),
2342        )
2343        .await;
2344
2345        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2346
2347        let (workspace, cx) =
2348            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2349
2350        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2351        let history = cx
2352            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2353
2354        let message_editor = cx.update(|window, cx| {
2355            cx.new(|cx| {
2356                let editor = MessageEditor::new_with_cache(
2357                    workspace.downgrade(),
2358                    project.downgrade(),
2359                    thread_store.clone(),
2360                    history.downgrade(),
2361                    None,
2362                    Default::default(),
2363                    Default::default(),
2364                    Default::default(),
2365                    Default::default(),
2366                    "Test Agent".into(),
2367                    "Test",
2368                    EditorMode::AutoHeight {
2369                        min_lines: 1,
2370                        max_lines: None,
2371                    },
2372                    window,
2373                    cx,
2374                );
2375                // Enable embedded context so files are actually included
2376                editor
2377                    .prompt_capabilities
2378                    .replace(acp::PromptCapabilities::new().embedded_context(true));
2379                editor
2380            })
2381        });
2382
2383        // Test large file mention
2384        // Get the absolute path using the project's worktree
2385        let large_file_abs_path = project.read_with(cx, |project, cx| {
2386            let worktree = project.worktrees(cx).next().unwrap();
2387            let worktree_root = worktree.read(cx).abs_path();
2388            worktree_root.join("large_file.txt")
2389        });
2390        let large_file_task = message_editor.update(cx, |editor, cx| {
2391            editor.mention_set().update(cx, |set, cx| {
2392                set.confirm_mention_for_file(large_file_abs_path, true, cx)
2393            })
2394        });
2395
2396        let large_file_mention = large_file_task.await.unwrap();
2397        match large_file_mention {
2398            Mention::Text { content, .. } => {
2399                // Should contain some of the content but not all of it
2400                assert!(
2401                    content.contains(LINE),
2402                    "Should contain some of the file content"
2403                );
2404                assert!(
2405                    !content.contains(&LINE.repeat(100)),
2406                    "Should not contain the full file"
2407                );
2408                // Should be much smaller than original
2409                assert!(
2410                    content.len() < large_content.len() / 10,
2411                    "Should be significantly truncated"
2412                );
2413            }
2414            _ => panic!("Expected Text mention for large file"),
2415        }
2416
2417        // Test small file mention
2418        // Get the absolute path using the project's worktree
2419        let small_file_abs_path = project.read_with(cx, |project, cx| {
2420            let worktree = project.worktrees(cx).next().unwrap();
2421            let worktree_root = worktree.read(cx).abs_path();
2422            worktree_root.join("small_file.txt")
2423        });
2424        let small_file_task = message_editor.update(cx, |editor, cx| {
2425            editor.mention_set().update(cx, |set, cx| {
2426                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2427            })
2428        });
2429
2430        let small_file_mention = small_file_task.await.unwrap();
2431        match small_file_mention {
2432            Mention::Text { content, .. } => {
2433                // Should contain the full actual content
2434                assert_eq!(content, small_content);
2435            }
2436            _ => panic!("Expected Text mention for small file"),
2437        }
2438    }
2439
2440    #[gpui::test]
2441    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2442        init_test(cx);
2443        cx.update(LanguageModelRegistry::test);
2444
2445        let fs = FakeFs::new(cx.executor());
2446        fs.insert_tree("/project", json!({"file": ""})).await;
2447        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2448
2449        let (workspace, cx) =
2450            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2451
2452        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2453        let history = cx
2454            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2455
2456        // Create a thread metadata to insert as summary
2457        let thread_metadata = AgentSessionInfo {
2458            session_id: acp::SessionId::new("thread-123"),
2459            cwd: None,
2460            title: Some("Previous Conversation".into()),
2461            updated_at: Some(chrono::Utc::now()),
2462            meta: None,
2463        };
2464
2465        let message_editor = cx.update(|window, cx| {
2466            cx.new(|cx| {
2467                let mut editor = MessageEditor::new_with_cache(
2468                    workspace.downgrade(),
2469                    project.downgrade(),
2470                    thread_store.clone(),
2471                    history.downgrade(),
2472                    None,
2473                    Default::default(),
2474                    Default::default(),
2475                    Default::default(),
2476                    Default::default(),
2477                    "Test Agent".into(),
2478                    "Test",
2479                    EditorMode::AutoHeight {
2480                        min_lines: 1,
2481                        max_lines: None,
2482                    },
2483                    window,
2484                    cx,
2485                );
2486                editor.insert_thread_summary(thread_metadata.clone(), window, cx);
2487                editor
2488            })
2489        });
2490
2491        // Construct expected values for verification
2492        let expected_uri = MentionUri::Thread {
2493            id: thread_metadata.session_id.clone(),
2494            name: thread_metadata.title.as_ref().unwrap().to_string(),
2495        };
2496        let expected_title = thread_metadata.title.as_ref().unwrap();
2497        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
2498
2499        message_editor.read_with(cx, |editor, cx| {
2500            let text = editor.text(cx);
2501
2502            assert!(
2503                text.contains(&expected_link),
2504                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2505                expected_link,
2506                text
2507            );
2508
2509            let mentions = editor.mention_set().read(cx).mentions();
2510            assert_eq!(
2511                mentions.len(),
2512                1,
2513                "Expected exactly one mention after inserting thread summary"
2514            );
2515
2516            assert!(
2517                mentions.contains(&expected_uri),
2518                "Expected mentions to contain the thread URI"
2519            );
2520        });
2521    }
2522
2523    #[gpui::test]
2524    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
2525        init_test(cx);
2526        cx.update(LanguageModelRegistry::test);
2527
2528        let fs = FakeFs::new(cx.executor());
2529        fs.insert_tree("/project", json!({"file": ""})).await;
2530        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2531
2532        let (workspace, cx) =
2533            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2534
2535        let thread_store = None;
2536        let history = cx
2537            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2538
2539        let thread_metadata = AgentSessionInfo {
2540            session_id: acp::SessionId::new("thread-123"),
2541            cwd: None,
2542            title: Some("Previous Conversation".into()),
2543            updated_at: Some(chrono::Utc::now()),
2544            meta: None,
2545        };
2546
2547        let message_editor = cx.update(|window, cx| {
2548            cx.new(|cx| {
2549                let mut editor = MessageEditor::new_with_cache(
2550                    workspace.downgrade(),
2551                    project.downgrade(),
2552                    thread_store.clone(),
2553                    history.downgrade(),
2554                    None,
2555                    Default::default(),
2556                    Default::default(),
2557                    Default::default(),
2558                    Default::default(),
2559                    "Test Agent".into(),
2560                    "Test",
2561                    EditorMode::AutoHeight {
2562                        min_lines: 1,
2563                        max_lines: None,
2564                    },
2565                    window,
2566                    cx,
2567                );
2568                editor.insert_thread_summary(thread_metadata, window, cx);
2569                editor
2570            })
2571        });
2572
2573        message_editor.read_with(cx, |editor, cx| {
2574            assert!(
2575                editor.text(cx).is_empty(),
2576                "Expected thread summary to be skipped for external agents"
2577            );
2578            assert!(
2579                editor.mention_set().read(cx).mentions().is_empty(),
2580                "Expected no mentions when thread summary is skipped"
2581            );
2582        });
2583    }
2584
2585    #[gpui::test]
2586    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
2587        init_test(cx);
2588
2589        let fs = FakeFs::new(cx.executor());
2590        fs.insert_tree("/project", json!({"file": ""})).await;
2591        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2592
2593        let (workspace, cx) =
2594            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2595
2596        let thread_store = None;
2597        let history = cx
2598            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2599
2600        let message_editor = cx.update(|window, cx| {
2601            cx.new(|cx| {
2602                MessageEditor::new_with_cache(
2603                    workspace.downgrade(),
2604                    project.downgrade(),
2605                    thread_store.clone(),
2606                    history.downgrade(),
2607                    None,
2608                    Default::default(),
2609                    Default::default(),
2610                    Default::default(),
2611                    Default::default(),
2612                    "Test Agent".into(),
2613                    "Test",
2614                    EditorMode::AutoHeight {
2615                        min_lines: 1,
2616                        max_lines: None,
2617                    },
2618                    window,
2619                    cx,
2620                )
2621            })
2622        });
2623
2624        message_editor.update(cx, |editor, _cx| {
2625            editor
2626                .prompt_capabilities
2627                .replace(acp::PromptCapabilities::new().embedded_context(true));
2628        });
2629
2630        let supported_modes = {
2631            let app = cx.app.borrow();
2632            message_editor.supported_modes(&app)
2633        };
2634
2635        assert!(
2636            !supported_modes.contains(&PromptContextType::Thread),
2637            "Expected thread mode to be hidden when thread mentions are disabled"
2638        );
2639    }
2640
2641    #[gpui::test]
2642    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
2643        init_test(cx);
2644
2645        let fs = FakeFs::new(cx.executor());
2646        fs.insert_tree("/project", json!({"file": ""})).await;
2647        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2648
2649        let (workspace, cx) =
2650            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2651
2652        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2653        let history = cx
2654            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2655
2656        let message_editor = cx.update(|window, cx| {
2657            cx.new(|cx| {
2658                MessageEditor::new_with_cache(
2659                    workspace.downgrade(),
2660                    project.downgrade(),
2661                    thread_store.clone(),
2662                    history.downgrade(),
2663                    None,
2664                    Default::default(),
2665                    Default::default(),
2666                    Default::default(),
2667                    Default::default(),
2668                    "Test Agent".into(),
2669                    "Test",
2670                    EditorMode::AutoHeight {
2671                        min_lines: 1,
2672                        max_lines: None,
2673                    },
2674                    window,
2675                    cx,
2676                )
2677            })
2678        });
2679
2680        message_editor.update(cx, |editor, _cx| {
2681            editor
2682                .prompt_capabilities
2683                .replace(acp::PromptCapabilities::new().embedded_context(true));
2684        });
2685
2686        let supported_modes = {
2687            let app = cx.app.borrow();
2688            message_editor.supported_modes(&app)
2689        };
2690
2691        assert!(
2692            supported_modes.contains(&PromptContextType::Thread),
2693            "Expected thread mode to be visible when enabled"
2694        );
2695    }
2696
2697    #[gpui::test]
2698    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
2699        init_test(cx);
2700
2701        let fs = FakeFs::new(cx.executor());
2702        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
2703            .await;
2704        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2705
2706        let (workspace, cx) =
2707            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2708
2709        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2710        let history = cx
2711            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2712
2713        let message_editor = cx.update(|window, cx| {
2714            cx.new(|cx| {
2715                MessageEditor::new_with_cache(
2716                    workspace.downgrade(),
2717                    project.downgrade(),
2718                    thread_store.clone(),
2719                    history.downgrade(),
2720                    None,
2721                    Default::default(),
2722                    Default::default(),
2723                    Default::default(),
2724                    Default::default(),
2725                    "Test Agent".into(),
2726                    "Test",
2727                    EditorMode::AutoHeight {
2728                        min_lines: 1,
2729                        max_lines: None,
2730                    },
2731                    window,
2732                    cx,
2733                )
2734            })
2735        });
2736        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2737
2738        cx.run_until_parked();
2739
2740        editor.update_in(cx, |editor, window, cx| {
2741            editor.set_text("  \u{A0}してhello world  ", window, cx);
2742        });
2743
2744        let (content, _) = message_editor
2745            .update(cx, |message_editor, cx| {
2746                message_editor.contents_with_cache(false, None, None, cx)
2747            })
2748            .await
2749            .unwrap();
2750
2751        assert_eq!(content, vec!["してhello world".into()]);
2752    }
2753
2754    #[gpui::test]
2755    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
2756        init_test(cx);
2757
2758        let fs = FakeFs::new(cx.executor());
2759
2760        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
2761
2762        fs.insert_tree(
2763            "/project",
2764            json!({
2765                "src": {
2766                    "main.rs": file_content,
2767                }
2768            }),
2769        )
2770        .await;
2771
2772        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2773
2774        let (workspace, cx) =
2775            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2776
2777        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2778        let history = cx
2779            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2780
2781        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
2782            let workspace_handle = cx.weak_entity();
2783            let message_editor = cx.new(|cx| {
2784                MessageEditor::new_with_cache(
2785                    workspace_handle,
2786                    project.downgrade(),
2787                    thread_store.clone(),
2788                    history.downgrade(),
2789                    None,
2790                    Default::default(),
2791                    Default::default(),
2792                    Default::default(),
2793                    Default::default(),
2794                    "Test Agent".into(),
2795                    "Test",
2796                    EditorMode::AutoHeight {
2797                        max_lines: None,
2798                        min_lines: 1,
2799                    },
2800                    window,
2801                    cx,
2802                )
2803            });
2804            workspace.active_pane().update(cx, |pane, cx| {
2805                pane.add_item(
2806                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2807                    true,
2808                    true,
2809                    None,
2810                    window,
2811                    cx,
2812                );
2813            });
2814            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2815            let editor = message_editor.read(cx).editor().clone();
2816            (message_editor, editor)
2817        });
2818
2819        cx.simulate_input("What is in @file main");
2820
2821        editor.update_in(cx, |editor, window, cx| {
2822            assert!(editor.has_visible_completions_menu());
2823            assert_eq!(editor.text(cx), "What is in @file main");
2824            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2825        });
2826
2827        let content = message_editor
2828            .update(cx, |editor, cx| {
2829                editor.contents_with_cache(false, None, None, cx)
2830            })
2831            .await
2832            .unwrap()
2833            .0;
2834
2835        let main_rs_uri = if cfg!(windows) {
2836            "file:///C:/project/src/main.rs"
2837        } else {
2838            "file:///project/src/main.rs"
2839        };
2840
2841        // When embedded context is `false` we should get a resource link
2842        pretty_assertions::assert_eq!(
2843            content,
2844            vec![
2845                "What is in ".into(),
2846                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
2847            ]
2848        );
2849
2850        message_editor.update(cx, |editor, _cx| {
2851            editor
2852                .prompt_capabilities
2853                .replace(acp::PromptCapabilities::new().embedded_context(true))
2854        });
2855
2856        let content = message_editor
2857            .update(cx, |editor, cx| {
2858                editor.contents_with_cache(false, None, None, cx)
2859            })
2860            .await
2861            .unwrap()
2862            .0;
2863
2864        // When embedded context is `true` we should get a resource
2865        pretty_assertions::assert_eq!(
2866            content,
2867            vec![
2868                "What is in ".into(),
2869                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
2870                    acp::EmbeddedResourceResource::TextResourceContents(
2871                        acp::TextResourceContents::new(file_content, main_rs_uri)
2872                    )
2873                ))
2874            ]
2875        );
2876    }
2877
2878    #[gpui::test]
2879    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
2880        init_test(cx);
2881
2882        let app_state = cx.update(AppState::test);
2883
2884        cx.update(|cx| {
2885            editor::init(cx);
2886            workspace::init(app_state.clone(), cx);
2887        });
2888
2889        app_state
2890            .fs
2891            .as_fake()
2892            .insert_tree(
2893                path!("/dir"),
2894                json!({
2895                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
2896                }),
2897            )
2898            .await;
2899
2900        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2901        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2902        let workspace = window.root(cx).unwrap();
2903
2904        let worktree = project.update(cx, |project, cx| {
2905            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2906            assert_eq!(worktrees.len(), 1);
2907            worktrees.pop().unwrap()
2908        });
2909        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2910
2911        let mut cx = VisualTestContext::from_window(*window, cx);
2912
2913        // Open a regular editor with the created file, and select a portion of
2914        // the text that will be used for the selections that are meant to be
2915        // inserted in the agent panel.
2916        let editor = workspace
2917            .update_in(&mut cx, |workspace, window, cx| {
2918                workspace.open_path(
2919                    ProjectPath {
2920                        worktree_id,
2921                        path: rel_path("test.txt").into(),
2922                    },
2923                    None,
2924                    false,
2925                    window,
2926                    cx,
2927                )
2928            })
2929            .await
2930            .unwrap()
2931            .downcast::<Editor>()
2932            .unwrap();
2933
2934        editor.update_in(&mut cx, |editor, window, cx| {
2935            editor.change_selections(Default::default(), window, cx, |selections| {
2936                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
2937            });
2938        });
2939
2940        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2941        let history = cx
2942            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2943
2944        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
2945        // to ensure we have a fixed viewport, so we can eventually actually
2946        // place the cursor outside of the visible area.
2947        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2948            let workspace_handle = cx.weak_entity();
2949            let message_editor = cx.new(|cx| {
2950                MessageEditor::new_with_cache(
2951                    workspace_handle,
2952                    project.downgrade(),
2953                    thread_store.clone(),
2954                    history.downgrade(),
2955                    None,
2956                    Default::default(),
2957                    Default::default(),
2958                    Default::default(),
2959                    Default::default(),
2960                    "Test Agent".into(),
2961                    "Test",
2962                    EditorMode::full(),
2963                    window,
2964                    cx,
2965                )
2966            });
2967            workspace.active_pane().update(cx, |pane, cx| {
2968                pane.add_item(
2969                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2970                    true,
2971                    true,
2972                    None,
2973                    window,
2974                    cx,
2975                );
2976            });
2977
2978            message_editor
2979        });
2980
2981        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2982            message_editor.editor.update(cx, |editor, cx| {
2983                // Update the Agent Panel's Message Editor text to have 100
2984                // lines, ensuring that the cursor is set at line 90 and that we
2985                // then scroll all the way to the top, so the cursor's position
2986                // remains off screen.
2987                let mut lines = String::new();
2988                for _ in 1..=100 {
2989                    lines.push_str(&"Another line in the agent panel's message editor\n");
2990                }
2991                editor.set_text(lines.as_str(), window, cx);
2992                editor.change_selections(Default::default(), window, cx, |selections| {
2993                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
2994                });
2995                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
2996            });
2997        });
2998
2999        cx.run_until_parked();
3000
3001        // Before proceeding, let's assert that the cursor is indeed off screen,
3002        // otherwise the rest of the test doesn't make sense.
3003        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3004            message_editor.editor.update(cx, |editor, cx| {
3005                let snapshot = editor.snapshot(window, cx);
3006                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3007                let scroll_top = snapshot.scroll_position().y as u32;
3008                let visible_lines = editor.visible_line_count().unwrap() as u32;
3009                let visible_range = scroll_top..(scroll_top + visible_lines);
3010
3011                assert!(!visible_range.contains(&cursor_row));
3012            })
3013        });
3014
3015        // Now let's insert the selection in the Agent Panel's editor and
3016        // confirm that, after the insertion, the cursor is now in the visible
3017        // range.
3018        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3019            message_editor.insert_selections(window, cx);
3020        });
3021
3022        cx.run_until_parked();
3023
3024        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3025            message_editor.editor.update(cx, |editor, cx| {
3026                let snapshot = editor.snapshot(window, cx);
3027                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3028                let scroll_top = snapshot.scroll_position().y as u32;
3029                let visible_lines = editor.visible_line_count().unwrap() as u32;
3030                let visible_range = scroll_top..(scroll_top + visible_lines);
3031
3032                assert!(visible_range.contains(&cursor_row));
3033            })
3034        });
3035    }
3036}