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        self.insert_context_prefix("@", window, cx);
 676    }
 677
 678    pub fn insert_context_type(
 679        &mut self,
 680        context_keyword: &str,
 681        window: &mut Window,
 682        cx: &mut Context<Self>,
 683    ) {
 684        let prefix = format!("@{}", context_keyword);
 685        self.insert_context_prefix(&prefix, window, cx);
 686    }
 687
 688    fn insert_context_prefix(&mut self, prefix: &str, window: &mut Window, cx: &mut Context<Self>) {
 689        let editor = self.editor.clone();
 690        let prefix = prefix.to_string();
 691
 692        cx.spawn_in(window, async move |_, cx| {
 693            editor
 694                .update_in(cx, |editor, window, cx| {
 695                    let menu_is_open =
 696                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 697                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 698                        });
 699
 700                    let has_prefix = {
 701                        let snapshot = editor.display_snapshot(cx);
 702                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 703                        let offset = cursor.to_offset(&snapshot);
 704                        if offset.0 >= prefix.len() {
 705                            let start_offset = MultiBufferOffset(offset.0 - prefix.len());
 706                            let buffer_snapshot = snapshot.buffer_snapshot();
 707                            let text = buffer_snapshot
 708                                .text_for_range(start_offset..offset)
 709                                .collect::<String>();
 710                            text == prefix
 711                        } else {
 712                            false
 713                        }
 714                    };
 715
 716                    if menu_is_open && has_prefix {
 717                        return;
 718                    }
 719
 720                    editor.insert(&prefix, window, cx);
 721                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 722                })
 723                .log_err();
 724        })
 725        .detach();
 726    }
 727
 728    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 729        self.send(cx);
 730    }
 731
 732    fn send_immediately(&mut self, _: &SendImmediately, _: &mut Window, cx: &mut Context<Self>) {
 733        if self.is_empty(cx) {
 734            return;
 735        }
 736
 737        self.editor.update(cx, |editor, cx| {
 738            editor.clear_inlay_hints(cx);
 739        });
 740
 741        cx.emit(MessageEditorEvent::SendImmediately)
 742    }
 743
 744    fn chat_with_follow(
 745        &mut self,
 746        _: &ChatWithFollow,
 747        window: &mut Window,
 748        cx: &mut Context<Self>,
 749    ) {
 750        self.workspace
 751            .update(cx, |this, cx| {
 752                this.follow(CollaboratorId::Agent, window, cx)
 753            })
 754            .log_err();
 755
 756        self.send(cx);
 757    }
 758
 759    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 760        cx.emit(MessageEditorEvent::Cancel)
 761    }
 762
 763    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 764        let Some(workspace) = self.workspace.upgrade() else {
 765            return;
 766        };
 767        let editor_clipboard_selections = cx
 768            .read_from_clipboard()
 769            .and_then(|item| item.entries().first().cloned())
 770            .and_then(|entry| match entry {
 771                ClipboardEntry::String(text) => {
 772                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
 773                }
 774                _ => None,
 775            });
 776
 777        // Insert creases for pasted clipboard selections that:
 778        // 1. Contain exactly one selection
 779        // 2. Have an associated file path
 780        // 3. Span multiple lines (not single-line selections)
 781        // 4. Belong to a file that exists in the current project
 782        let should_insert_creases = util::maybe!({
 783            let selections = editor_clipboard_selections.as_ref()?;
 784            if selections.len() > 1 {
 785                return Some(false);
 786            }
 787            let selection = selections.first()?;
 788            let file_path = selection.file_path.as_ref()?;
 789            let line_range = selection.line_range.as_ref()?;
 790
 791            if line_range.start() == line_range.end() {
 792                return Some(false);
 793            }
 794
 795            Some(
 796                workspace
 797                    .read(cx)
 798                    .project()
 799                    .read(cx)
 800                    .project_path_for_absolute_path(file_path, cx)
 801                    .is_some(),
 802            )
 803        })
 804        .unwrap_or(false);
 805
 806        if should_insert_creases && let Some(selections) = editor_clipboard_selections {
 807            cx.stop_propagation();
 808            let insertion_target = self
 809                .editor
 810                .read(cx)
 811                .selections
 812                .newest_anchor()
 813                .start
 814                .text_anchor;
 815
 816            let project = workspace.read(cx).project().clone();
 817            for selection in selections {
 818                if let (Some(file_path), Some(line_range)) =
 819                    (selection.file_path, selection.line_range)
 820                {
 821                    let crease_text =
 822                        acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
 823
 824                    let mention_uri = MentionUri::Selection {
 825                        abs_path: Some(file_path.clone()),
 826                        line_range: line_range.clone(),
 827                    };
 828
 829                    let mention_text = mention_uri.as_link().to_string();
 830                    let (excerpt_id, text_anchor, content_len) =
 831                        self.editor.update(cx, |editor, cx| {
 832                            let buffer = editor.buffer().read(cx);
 833                            let snapshot = buffer.snapshot(cx);
 834                            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
 835                            let text_anchor = insertion_target.bias_left(&buffer_snapshot);
 836
 837                            editor.insert(&mention_text, window, cx);
 838                            editor.insert(" ", window, cx);
 839
 840                            (*excerpt_id, text_anchor, mention_text.len())
 841                        });
 842
 843                    let Some((crease_id, tx)) = insert_crease_for_mention(
 844                        excerpt_id,
 845                        text_anchor,
 846                        content_len,
 847                        crease_text.into(),
 848                        mention_uri.icon_path(cx),
 849                        None,
 850                        self.editor.clone(),
 851                        window,
 852                        cx,
 853                    ) else {
 854                        continue;
 855                    };
 856                    drop(tx);
 857
 858                    let mention_task = cx
 859                        .spawn({
 860                            let project = project.clone();
 861                            async move |_, cx| {
 862                                let project_path = project
 863                                    .update(cx, |project, cx| {
 864                                        project.project_path_for_absolute_path(&file_path, cx)
 865                                    })
 866                                    .ok_or_else(|| "project path not found".to_string())?;
 867
 868                                let buffer = project
 869                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
 870                                    .await
 871                                    .map_err(|e| e.to_string())?;
 872
 873                                Ok(buffer.update(cx, |buffer, cx| {
 874                                    let start =
 875                                        Point::new(*line_range.start(), 0).min(buffer.max_point());
 876                                    let end = Point::new(*line_range.end() + 1, 0)
 877                                        .min(buffer.max_point());
 878                                    let content = buffer.text_for_range(start..end).collect();
 879                                    Mention::Text {
 880                                        content,
 881                                        tracked_buffers: vec![cx.entity()],
 882                                    }
 883                                }))
 884                            }
 885                        })
 886                        .shared();
 887
 888                    self.mention_set.update(cx, |mention_set, _cx| {
 889                        mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
 890                    });
 891                }
 892            }
 893            return;
 894        }
 895
 896        if self.prompt_capabilities.borrow().image
 897            && let Some(task) =
 898                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
 899        {
 900            task.detach();
 901        }
 902    }
 903
 904    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
 905        let editor = self.editor.clone();
 906        window.defer(cx, move |window, cx| {
 907            editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
 908        });
 909    }
 910
 911    pub fn insert_dragged_files(
 912        &mut self,
 913        paths: Vec<project::ProjectPath>,
 914        added_worktrees: Vec<Entity<Worktree>>,
 915        window: &mut Window,
 916        cx: &mut Context<Self>,
 917    ) {
 918        let Some(workspace) = self.workspace.upgrade() else {
 919            return;
 920        };
 921        let project = workspace.read(cx).project().clone();
 922        let path_style = project.read(cx).path_style(cx);
 923        let buffer = self.editor.read(cx).buffer().clone();
 924        let Some(buffer) = buffer.read(cx).as_singleton() else {
 925            return;
 926        };
 927        let mut tasks = Vec::new();
 928        for path in paths {
 929            let Some(entry) = project.read(cx).entry_for_path(&path, cx) else {
 930                continue;
 931            };
 932            let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 933                continue;
 934            };
 935            let abs_path = worktree.read(cx).absolutize(&path.path);
 936            let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 937                &path.path,
 938                worktree.read(cx).root_name(),
 939                path_style,
 940            );
 941
 942            let uri = if entry.is_dir() {
 943                MentionUri::Directory { abs_path }
 944            } else {
 945                MentionUri::File { abs_path }
 946            };
 947
 948            let new_text = format!("{} ", uri.as_link());
 949            let content_len = new_text.len() - 1;
 950
 951            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 952
 953            self.editor.update(cx, |message_editor, cx| {
 954                message_editor.edit(
 955                    [(
 956                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 957                        new_text,
 958                    )],
 959                    cx,
 960                );
 961            });
 962            let supports_images = self.prompt_capabilities.borrow().image;
 963            tasks.push(self.mention_set.update(cx, |mention_set, cx| {
 964                mention_set.confirm_mention_completion(
 965                    file_name,
 966                    anchor,
 967                    content_len,
 968                    uri,
 969                    supports_images,
 970                    self.editor.clone(),
 971                    &workspace,
 972                    window,
 973                    cx,
 974                )
 975            }));
 976        }
 977        cx.spawn(async move |_, _| {
 978            join_all(tasks).await;
 979            drop(added_worktrees);
 980        })
 981        .detach();
 982    }
 983
 984    /// Inserts code snippets as creases into the editor.
 985    /// Each tuple contains (code_text, crease_title).
 986    pub fn insert_code_creases(
 987        &mut self,
 988        creases: Vec<(String, String)>,
 989        window: &mut Window,
 990        cx: &mut Context<Self>,
 991    ) {
 992        use editor::display_map::{Crease, FoldPlaceholder};
 993        use multi_buffer::MultiBufferRow;
 994        use rope::Point;
 995
 996        self.editor.update(cx, |editor, cx| {
 997            editor.insert("\n", window, cx);
 998            for (text, crease_title) in creases {
 999                let point = editor
1000                    .selections
1001                    .newest::<Point>(&editor.display_snapshot(cx))
1002                    .head();
1003                let start_row = MultiBufferRow(point.row);
1004
1005                editor.insert(&text, window, cx);
1006
1007                let snapshot = editor.buffer().read(cx).snapshot(cx);
1008                let anchor_before = snapshot.anchor_after(point);
1009                let anchor_after = editor
1010                    .selections
1011                    .newest_anchor()
1012                    .head()
1013                    .bias_left(&snapshot);
1014
1015                editor.insert("\n", window, cx);
1016
1017                let fold_placeholder = FoldPlaceholder {
1018                    render: Arc::new({
1019                        let title = crease_title.clone();
1020                        move |_fold_id, _fold_range, _cx| {
1021                            ButtonLike::new("code-crease")
1022                                .style(ButtonStyle::Filled)
1023                                .layer(ElevationIndex::ElevatedSurface)
1024                                .child(Icon::new(IconName::TextSnippet))
1025                                .child(Label::new(title.clone()).single_line())
1026                                .into_any_element()
1027                        }
1028                    }),
1029                    merge_adjacent: false,
1030                    ..Default::default()
1031                };
1032
1033                let crease = Crease::inline(
1034                    anchor_before..anchor_after,
1035                    fold_placeholder,
1036                    |row, is_folded, fold, _window, _cx| {
1037                        Disclosure::new(("code-crease-toggle", row.0 as u64), !is_folded)
1038                            .toggle_state(is_folded)
1039                            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
1040                            .into_any_element()
1041                    },
1042                    |_, _, _, _| gpui::Empty.into_any(),
1043                );
1044                editor.insert_creases(vec![crease], cx);
1045                editor.fold_at(start_row, window, cx);
1046            }
1047        });
1048    }
1049
1050    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1051        let editor = self.editor.read(cx);
1052        let editor_buffer = editor.buffer().read(cx);
1053        let Some(buffer) = editor_buffer.as_singleton() else {
1054            return;
1055        };
1056        let cursor_anchor = editor.selections.newest_anchor().head();
1057        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1058        let anchor = buffer.update(cx, |buffer, _cx| {
1059            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
1060        });
1061        let Some(workspace) = self.workspace.upgrade() else {
1062            return;
1063        };
1064        let Some(completion) =
1065            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
1066                PromptContextAction::AddSelections,
1067                anchor..anchor,
1068                self.editor.downgrade(),
1069                self.mention_set.downgrade(),
1070                &workspace,
1071                cx,
1072            )
1073        else {
1074            return;
1075        };
1076
1077        self.editor.update(cx, |message_editor, cx| {
1078            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1079            message_editor.request_autoscroll(Autoscroll::fit(), cx);
1080        });
1081        if let Some(confirm) = completion.confirm {
1082            confirm(CompletionIntent::Complete, window, cx);
1083        }
1084    }
1085
1086    pub fn add_images_from_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1087        if !self.prompt_capabilities.borrow().image {
1088            return;
1089        }
1090
1091        let editor = self.editor.clone();
1092        let mention_set = self.mention_set.clone();
1093
1094        let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
1095            files: true,
1096            directories: false,
1097            multiple: true,
1098            prompt: Some("Select Images".into()),
1099        });
1100
1101        window
1102            .spawn(cx, async move |cx| {
1103                let paths = match paths_receiver.await {
1104                    Ok(Ok(Some(paths))) => paths,
1105                    _ => return Ok::<(), anyhow::Error>(()),
1106                };
1107
1108                let supported_formats = [
1109                    ("png", gpui::ImageFormat::Png),
1110                    ("jpg", gpui::ImageFormat::Jpeg),
1111                    ("jpeg", gpui::ImageFormat::Jpeg),
1112                    ("webp", gpui::ImageFormat::Webp),
1113                    ("gif", gpui::ImageFormat::Gif),
1114                    ("bmp", gpui::ImageFormat::Bmp),
1115                    ("tiff", gpui::ImageFormat::Tiff),
1116                    ("tif", gpui::ImageFormat::Tiff),
1117                    ("ico", gpui::ImageFormat::Ico),
1118                ];
1119
1120                let mut images = Vec::new();
1121                for path in paths {
1122                    let extension = path
1123                        .extension()
1124                        .and_then(|ext| ext.to_str())
1125                        .map(|s| s.to_lowercase());
1126
1127                    let Some(format) = extension.and_then(|ext| {
1128                        supported_formats
1129                            .iter()
1130                            .find(|(e, _)| *e == ext)
1131                            .map(|(_, f)| *f)
1132                    }) else {
1133                        continue;
1134                    };
1135
1136                    let Ok(content) = async_fs::read(&path).await else {
1137                        continue;
1138                    };
1139
1140                    images.push(gpui::Image::from_bytes(format, content));
1141                }
1142
1143                crate::mention_set::insert_images_as_context(images, editor, mention_set, cx).await;
1144                Ok(())
1145            })
1146            .detach_and_log_err(cx);
1147    }
1148
1149    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1150        self.editor.update(cx, |message_editor, cx| {
1151            message_editor.set_read_only(read_only);
1152            cx.notify()
1153        })
1154    }
1155
1156    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1157        self.editor.update(cx, |editor, cx| {
1158            editor.set_mode(mode);
1159            cx.notify()
1160        });
1161    }
1162
1163    pub fn set_message(
1164        &mut self,
1165        message: Vec<acp::ContentBlock>,
1166        window: &mut Window,
1167        cx: &mut Context<Self>,
1168    ) {
1169        let Some(workspace) = self.workspace.upgrade() else {
1170            return;
1171        };
1172
1173        self.clear(window, cx);
1174
1175        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1176        let mut text = String::new();
1177        let mut mentions = Vec::new();
1178
1179        for chunk in message {
1180            match chunk {
1181                acp::ContentBlock::Text(text_content) => {
1182                    text.push_str(&text_content.text);
1183                }
1184                acp::ContentBlock::Resource(acp::EmbeddedResource {
1185                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1186                    ..
1187                }) => {
1188                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1189                    else {
1190                        continue;
1191                    };
1192                    let start = text.len();
1193                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1194                    let end = text.len();
1195                    mentions.push((
1196                        start..end,
1197                        mention_uri,
1198                        Mention::Text {
1199                            content: resource.text,
1200                            tracked_buffers: Vec::new(),
1201                        },
1202                    ));
1203                }
1204                acp::ContentBlock::ResourceLink(resource) => {
1205                    if let Some(mention_uri) =
1206                        MentionUri::parse(&resource.uri, path_style).log_err()
1207                    {
1208                        let start = text.len();
1209                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1210                        let end = text.len();
1211                        mentions.push((start..end, mention_uri, Mention::Link));
1212                    }
1213                }
1214                acp::ContentBlock::Image(acp::ImageContent {
1215                    uri,
1216                    data,
1217                    mime_type,
1218                    ..
1219                }) => {
1220                    let mention_uri = if let Some(uri) = uri {
1221                        MentionUri::parse(&uri, path_style)
1222                    } else {
1223                        Ok(MentionUri::PastedImage)
1224                    };
1225                    let Some(mention_uri) = mention_uri.log_err() else {
1226                        continue;
1227                    };
1228                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1229                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1230                        continue;
1231                    };
1232                    let start = text.len();
1233                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1234                    let end = text.len();
1235                    mentions.push((
1236                        start..end,
1237                        mention_uri,
1238                        Mention::Image(MentionImage {
1239                            data: data.into(),
1240                            format,
1241                        }),
1242                    ));
1243                }
1244                _ => {}
1245            }
1246        }
1247
1248        let snapshot = self.editor.update(cx, |editor, cx| {
1249            editor.set_text(text, window, cx);
1250            editor.buffer().read(cx).snapshot(cx)
1251        });
1252
1253        for (range, mention_uri, mention) in mentions {
1254            let anchor = snapshot.anchor_before(MultiBufferOffset(range.start));
1255            let Some((crease_id, tx)) = insert_crease_for_mention(
1256                anchor.excerpt_id,
1257                anchor.text_anchor,
1258                range.end - range.start,
1259                mention_uri.name().into(),
1260                mention_uri.icon_path(cx),
1261                None,
1262                self.editor.clone(),
1263                window,
1264                cx,
1265            ) else {
1266                continue;
1267            };
1268            drop(tx);
1269
1270            self.mention_set.update(cx, |mention_set, _cx| {
1271                mention_set.insert_mention(
1272                    crease_id,
1273                    mention_uri.clone(),
1274                    Task::ready(Ok(mention)).shared(),
1275                )
1276            });
1277        }
1278        cx.notify();
1279    }
1280
1281    pub fn text(&self, cx: &App) -> String {
1282        self.editor.read(cx).text(cx)
1283    }
1284
1285    pub fn set_placeholder_text(
1286        &mut self,
1287        placeholder: &str,
1288        window: &mut Window,
1289        cx: &mut Context<Self>,
1290    ) {
1291        self.editor.update(cx, |editor, cx| {
1292            editor.set_placeholder_text(placeholder, window, cx);
1293        });
1294    }
1295
1296    #[cfg(test)]
1297    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1298        self.editor.update(cx, |editor, cx| {
1299            editor.set_text(text, window, cx);
1300        });
1301    }
1302}
1303
1304impl Focusable for MessageEditor {
1305    fn focus_handle(&self, cx: &App) -> FocusHandle {
1306        self.editor.focus_handle(cx)
1307    }
1308}
1309
1310impl Render for MessageEditor {
1311    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1312        div()
1313            .key_context("MessageEditor")
1314            .on_action(cx.listener(Self::chat))
1315            .on_action(cx.listener(Self::send_immediately))
1316            .on_action(cx.listener(Self::chat_with_follow))
1317            .on_action(cx.listener(Self::cancel))
1318            .on_action(cx.listener(Self::paste_raw))
1319            .capture_action(cx.listener(Self::paste))
1320            .flex_1()
1321            .child({
1322                let settings = ThemeSettings::get_global(cx);
1323
1324                let text_style = TextStyle {
1325                    color: cx.theme().colors().text,
1326                    font_family: settings.buffer_font.family.clone(),
1327                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1328                    font_features: settings.buffer_font.features.clone(),
1329                    font_size: settings.agent_buffer_font_size(cx).into(),
1330                    line_height: relative(settings.buffer_line_height.value()),
1331                    ..Default::default()
1332                };
1333
1334                EditorElement::new(
1335                    &self.editor,
1336                    EditorStyle {
1337                        background: cx.theme().colors().editor_background,
1338                        local_player: cx.theme().players().local(),
1339                        text: text_style,
1340                        syntax: cx.theme().syntax().clone(),
1341                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1342                        ..Default::default()
1343                    },
1344                )
1345            })
1346    }
1347}
1348
1349pub struct MessageEditorAddon {}
1350
1351impl MessageEditorAddon {
1352    pub fn new() -> Self {
1353        Self {}
1354    }
1355}
1356
1357impl Addon for MessageEditorAddon {
1358    fn to_any(&self) -> &dyn std::any::Any {
1359        self
1360    }
1361
1362    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1363        Some(self)
1364    }
1365
1366    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1367        let settings = agent_settings::AgentSettings::get_global(cx);
1368        if settings.use_modifier_to_send {
1369            key_context.add("use_modifier_to_send");
1370        }
1371    }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1377
1378    use acp_thread::{AgentSessionInfo, MentionUri};
1379    use agent::{ThreadStore, outline};
1380    use agent_client_protocol as acp;
1381    use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset};
1382
1383    use fs::FakeFs;
1384    use futures::StreamExt as _;
1385    use gpui::{
1386        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1387    };
1388    use language_model::LanguageModelRegistry;
1389    use lsp::{CompletionContext, CompletionTriggerKind};
1390    use project::{CompletionIntent, Project, ProjectPath};
1391    use serde_json::json;
1392
1393    use text::Point;
1394    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1395    use util::{path, paths::PathStyle, rel_path::rel_path};
1396    use workspace::{AppState, Item, Workspace};
1397
1398    use crate::acp::{
1399        message_editor::{Mention, MessageEditor},
1400        thread_view::tests::init_test,
1401    };
1402    use crate::completion_provider::{PromptCompletionProviderDelegate, PromptContextType};
1403
1404    #[gpui::test]
1405    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1406        init_test(cx);
1407
1408        let fs = FakeFs::new(cx.executor());
1409        fs.insert_tree("/project", json!({"file": ""})).await;
1410        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1411
1412        let (workspace, cx) =
1413            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1414
1415        let thread_store = None;
1416        let history = cx
1417            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1418
1419        let message_editor = cx.update(|window, cx| {
1420            cx.new(|cx| {
1421                MessageEditor::new_with_cache(
1422                    workspace.downgrade(),
1423                    project.downgrade(),
1424                    thread_store.clone(),
1425                    history.downgrade(),
1426                    None,
1427                    Default::default(),
1428                    Default::default(),
1429                    Default::default(),
1430                    Default::default(),
1431                    "Test Agent".into(),
1432                    "Test",
1433                    EditorMode::AutoHeight {
1434                        min_lines: 1,
1435                        max_lines: None,
1436                    },
1437                    window,
1438                    cx,
1439                )
1440            })
1441        });
1442        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1443
1444        cx.run_until_parked();
1445
1446        let excerpt_id = editor.update(cx, |editor, cx| {
1447            editor
1448                .buffer()
1449                .read(cx)
1450                .excerpt_ids()
1451                .into_iter()
1452                .next()
1453                .unwrap()
1454        });
1455        let completions = editor.update_in(cx, |editor, window, cx| {
1456            editor.set_text("Hello @file ", window, cx);
1457            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1458            let completion_provider = editor.completion_provider().unwrap();
1459            completion_provider.completions(
1460                excerpt_id,
1461                &buffer,
1462                text::Anchor::MAX,
1463                CompletionContext {
1464                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1465                    trigger_character: Some("@".into()),
1466                },
1467                window,
1468                cx,
1469            )
1470        });
1471        let [_, completion]: [_; 2] = completions
1472            .await
1473            .unwrap()
1474            .into_iter()
1475            .flat_map(|response| response.completions)
1476            .collect::<Vec<_>>()
1477            .try_into()
1478            .unwrap();
1479
1480        editor.update_in(cx, |editor, window, cx| {
1481            let snapshot = editor.buffer().read(cx).snapshot(cx);
1482            let range = snapshot
1483                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1484                .unwrap();
1485            editor.edit([(range, completion.new_text)], cx);
1486            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1487        });
1488
1489        cx.run_until_parked();
1490
1491        // Backspace over the inserted crease (and the following space).
1492        editor.update_in(cx, |editor, window, cx| {
1493            editor.backspace(&Default::default(), window, cx);
1494            editor.backspace(&Default::default(), window, cx);
1495        });
1496
1497        let (content, _) = message_editor
1498            .update(cx, |message_editor, cx| {
1499                message_editor.contents_with_cache(false, None, None, cx)
1500            })
1501            .await
1502            .unwrap();
1503
1504        // We don't send a resource link for the deleted crease.
1505        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1506    }
1507
1508    #[gpui::test]
1509    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1510        init_test(cx);
1511        let fs = FakeFs::new(cx.executor());
1512        fs.insert_tree(
1513            "/test",
1514            json!({
1515                ".zed": {
1516                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1517                },
1518                "src": {
1519                    "main.rs": "fn main() {}",
1520                },
1521            }),
1522        )
1523        .await;
1524
1525        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1526        let thread_store = None;
1527        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1528        // Start with no available commands - simulating Claude which doesn't support slash commands
1529        let available_commands = Rc::new(RefCell::new(vec![]));
1530
1531        let (workspace, cx) =
1532            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1533        let history = cx
1534            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1535        let workspace_handle = workspace.downgrade();
1536        let message_editor = workspace.update_in(cx, |_, window, cx| {
1537            cx.new(|cx| {
1538                MessageEditor::new_with_cache(
1539                    workspace_handle.clone(),
1540                    project.downgrade(),
1541                    thread_store.clone(),
1542                    history.downgrade(),
1543                    None,
1544                    prompt_capabilities.clone(),
1545                    available_commands.clone(),
1546                    Default::default(),
1547                    Default::default(),
1548                    "Claude Code".into(),
1549                    "Test",
1550                    EditorMode::AutoHeight {
1551                        min_lines: 1,
1552                        max_lines: None,
1553                    },
1554                    window,
1555                    cx,
1556                )
1557            })
1558        });
1559        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1560
1561        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1562        editor.update_in(cx, |editor, window, cx| {
1563            editor.set_text("/file test.txt", window, cx);
1564        });
1565
1566        let contents_result = message_editor
1567            .update(cx, |message_editor, cx| {
1568                message_editor.contents_with_cache(false, None, None, cx)
1569            })
1570            .await;
1571
1572        // Should fail because available_commands is empty (no commands supported)
1573        assert!(contents_result.is_err());
1574        let error_message = contents_result.unwrap_err().to_string();
1575        assert!(error_message.contains("not supported by Claude Code"));
1576        assert!(error_message.contains("Available commands: none"));
1577
1578        // Now simulate Claude providing its list of available commands (which doesn't include file)
1579        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
1580
1581        // Test that unsupported slash commands trigger an error when we have a list of available commands
1582        editor.update_in(cx, |editor, window, cx| {
1583            editor.set_text("/file test.txt", window, cx);
1584        });
1585
1586        let contents_result = message_editor
1587            .update(cx, |message_editor, cx| {
1588                message_editor.contents_with_cache(false, None, None, cx)
1589            })
1590            .await;
1591
1592        assert!(contents_result.is_err());
1593        let error_message = contents_result.unwrap_err().to_string();
1594        assert!(error_message.contains("not supported by Claude Code"));
1595        assert!(error_message.contains("/file"));
1596        assert!(error_message.contains("Available commands: /help"));
1597
1598        // Test that supported commands work fine
1599        editor.update_in(cx, |editor, window, cx| {
1600            editor.set_text("/help", window, cx);
1601        });
1602
1603        let contents_result = message_editor
1604            .update(cx, |message_editor, cx| {
1605                message_editor.contents_with_cache(false, None, None, cx)
1606            })
1607            .await;
1608
1609        // Should succeed because /help is in available_commands
1610        assert!(contents_result.is_ok());
1611
1612        // Test that regular text works fine
1613        editor.update_in(cx, |editor, window, cx| {
1614            editor.set_text("Hello Claude!", window, cx);
1615        });
1616
1617        let (content, _) = message_editor
1618            .update(cx, |message_editor, cx| {
1619                message_editor.contents_with_cache(false, None, None, cx)
1620            })
1621            .await
1622            .unwrap();
1623
1624        assert_eq!(content.len(), 1);
1625        if let acp::ContentBlock::Text(text) = &content[0] {
1626            assert_eq!(text.text, "Hello Claude!");
1627        } else {
1628            panic!("Expected ContentBlock::Text");
1629        }
1630
1631        // Test that @ mentions still work
1632        editor.update_in(cx, |editor, window, cx| {
1633            editor.set_text("Check this @", window, cx);
1634        });
1635
1636        // The @ mention functionality should not be affected
1637        let (content, _) = message_editor
1638            .update(cx, |message_editor, cx| {
1639                message_editor.contents_with_cache(false, None, None, cx)
1640            })
1641            .await
1642            .unwrap();
1643
1644        assert_eq!(content.len(), 1);
1645        if let acp::ContentBlock::Text(text) = &content[0] {
1646            assert_eq!(text.text, "Check this @");
1647        } else {
1648            panic!("Expected ContentBlock::Text");
1649        }
1650    }
1651
1652    struct MessageEditorItem(Entity<MessageEditor>);
1653
1654    impl Item for MessageEditorItem {
1655        type Event = ();
1656
1657        fn include_in_nav_history() -> bool {
1658            false
1659        }
1660
1661        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1662            "Test".into()
1663        }
1664    }
1665
1666    impl EventEmitter<()> for MessageEditorItem {}
1667
1668    impl Focusable for MessageEditorItem {
1669        fn focus_handle(&self, cx: &App) -> FocusHandle {
1670            self.0.read(cx).focus_handle(cx)
1671        }
1672    }
1673
1674    impl Render for MessageEditorItem {
1675        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1676            self.0.clone().into_any_element()
1677        }
1678    }
1679
1680    #[gpui::test]
1681    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1682        init_test(cx);
1683
1684        let app_state = cx.update(AppState::test);
1685
1686        cx.update(|cx| {
1687            editor::init(cx);
1688            workspace::init(app_state.clone(), cx);
1689        });
1690
1691        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1692        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1693        let workspace = window.root(cx).unwrap();
1694
1695        let mut cx = VisualTestContext::from_window(*window, cx);
1696
1697        let thread_store = None;
1698        let history = cx
1699            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1700        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1701        let available_commands = Rc::new(RefCell::new(vec![
1702            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
1703            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
1704                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
1705                    "<name>",
1706                )),
1707            ),
1708        ]));
1709
1710        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1711            let workspace_handle = cx.weak_entity();
1712            let message_editor = cx.new(|cx| {
1713                MessageEditor::new_with_cache(
1714                    workspace_handle,
1715                    project.downgrade(),
1716                    thread_store.clone(),
1717                    history.downgrade(),
1718                    None,
1719                    prompt_capabilities.clone(),
1720                    available_commands.clone(),
1721                    Default::default(),
1722                    Default::default(),
1723                    "Test Agent".into(),
1724                    "Test",
1725                    EditorMode::AutoHeight {
1726                        max_lines: None,
1727                        min_lines: 1,
1728                    },
1729                    window,
1730                    cx,
1731                )
1732            });
1733            workspace.active_pane().update(cx, |pane, cx| {
1734                pane.add_item(
1735                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1736                    true,
1737                    true,
1738                    None,
1739                    window,
1740                    cx,
1741                );
1742            });
1743            message_editor.read(cx).focus_handle(cx).focus(window, cx);
1744            message_editor.read(cx).editor().clone()
1745        });
1746
1747        cx.simulate_input("/");
1748
1749        editor.update_in(&mut cx, |editor, window, cx| {
1750            assert_eq!(editor.text(cx), "/");
1751            assert!(editor.has_visible_completions_menu());
1752
1753            assert_eq!(
1754                current_completion_labels_with_documentation(editor),
1755                &[
1756                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1757                    ("say-hello".into(), "Say hello to whoever you want".into())
1758                ]
1759            );
1760            editor.set_text("", window, cx);
1761        });
1762
1763        cx.simulate_input("/qui");
1764
1765        editor.update_in(&mut cx, |editor, window, cx| {
1766            assert_eq!(editor.text(cx), "/qui");
1767            assert!(editor.has_visible_completions_menu());
1768
1769            assert_eq!(
1770                current_completion_labels_with_documentation(editor),
1771                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1772            );
1773            editor.set_text("", window, cx);
1774        });
1775
1776        editor.update_in(&mut cx, |editor, window, cx| {
1777            assert!(editor.has_visible_completions_menu());
1778            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1779        });
1780
1781        cx.run_until_parked();
1782
1783        editor.update_in(&mut cx, |editor, window, cx| {
1784            assert_eq!(editor.display_text(cx), "/quick-math ");
1785            assert!(!editor.has_visible_completions_menu());
1786            editor.set_text("", window, cx);
1787        });
1788
1789        cx.simulate_input("/say");
1790
1791        editor.update_in(&mut cx, |editor, _window, cx| {
1792            assert_eq!(editor.display_text(cx), "/say");
1793            assert!(editor.has_visible_completions_menu());
1794
1795            assert_eq!(
1796                current_completion_labels_with_documentation(editor),
1797                &[("say-hello".into(), "Say hello to whoever you want".into())]
1798            );
1799        });
1800
1801        editor.update_in(&mut cx, |editor, window, cx| {
1802            assert!(editor.has_visible_completions_menu());
1803            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1804        });
1805
1806        cx.run_until_parked();
1807
1808        editor.update_in(&mut cx, |editor, _window, cx| {
1809            assert_eq!(editor.text(cx), "/say-hello ");
1810            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1811            assert!(!editor.has_visible_completions_menu());
1812        });
1813
1814        cx.simulate_input("GPT5");
1815
1816        cx.run_until_parked();
1817
1818        editor.update_in(&mut cx, |editor, window, cx| {
1819            assert_eq!(editor.text(cx), "/say-hello GPT5");
1820            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
1821            assert!(!editor.has_visible_completions_menu());
1822
1823            // Delete argument
1824            for _ in 0..5 {
1825                editor.backspace(&editor::actions::Backspace, window, cx);
1826            }
1827        });
1828
1829        cx.run_until_parked();
1830
1831        editor.update_in(&mut cx, |editor, window, cx| {
1832            assert_eq!(editor.text(cx), "/say-hello");
1833            // Hint is visible because argument was deleted
1834            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1835
1836            // Delete last command letter
1837            editor.backspace(&editor::actions::Backspace, window, cx);
1838        });
1839
1840        cx.run_until_parked();
1841
1842        editor.update_in(&mut cx, |editor, _window, cx| {
1843            // Hint goes away once command no longer matches an available one
1844            assert_eq!(editor.text(cx), "/say-hell");
1845            assert_eq!(editor.display_text(cx), "/say-hell");
1846            assert!(!editor.has_visible_completions_menu());
1847        });
1848    }
1849
1850    #[gpui::test]
1851    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
1852        init_test(cx);
1853
1854        let app_state = cx.update(AppState::test);
1855
1856        cx.update(|cx| {
1857            editor::init(cx);
1858            workspace::init(app_state.clone(), cx);
1859        });
1860
1861        app_state
1862            .fs
1863            .as_fake()
1864            .insert_tree(
1865                path!("/dir"),
1866                json!({
1867                    "editor": "",
1868                    "a": {
1869                        "one.txt": "1",
1870                        "two.txt": "2",
1871                        "three.txt": "3",
1872                        "four.txt": "4"
1873                    },
1874                    "b": {
1875                        "five.txt": "5",
1876                        "six.txt": "6",
1877                        "seven.txt": "7",
1878                        "eight.txt": "8",
1879                    },
1880                    "x.png": "",
1881                }),
1882            )
1883            .await;
1884
1885        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1886        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1887        let workspace = window.root(cx).unwrap();
1888
1889        let worktree = project.update(cx, |project, cx| {
1890            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1891            assert_eq!(worktrees.len(), 1);
1892            worktrees.pop().unwrap()
1893        });
1894        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1895
1896        let mut cx = VisualTestContext::from_window(*window, cx);
1897
1898        let paths = vec![
1899            rel_path("a/one.txt"),
1900            rel_path("a/two.txt"),
1901            rel_path("a/three.txt"),
1902            rel_path("a/four.txt"),
1903            rel_path("b/five.txt"),
1904            rel_path("b/six.txt"),
1905            rel_path("b/seven.txt"),
1906            rel_path("b/eight.txt"),
1907        ];
1908
1909        let slash = PathStyle::local().primary_separator();
1910
1911        let mut opened_editors = Vec::new();
1912        for path in paths {
1913            let buffer = workspace
1914                .update_in(&mut cx, |workspace, window, cx| {
1915                    workspace.open_path(
1916                        ProjectPath {
1917                            worktree_id,
1918                            path: path.into(),
1919                        },
1920                        None,
1921                        false,
1922                        window,
1923                        cx,
1924                    )
1925                })
1926                .await
1927                .unwrap();
1928            opened_editors.push(buffer);
1929        }
1930
1931        let thread_store = cx.new(|cx| ThreadStore::new(cx));
1932        let history = cx
1933            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
1934        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1935
1936        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1937            let workspace_handle = cx.weak_entity();
1938            let message_editor = cx.new(|cx| {
1939                MessageEditor::new_with_cache(
1940                    workspace_handle,
1941                    project.downgrade(),
1942                    Some(thread_store),
1943                    history.downgrade(),
1944                    None,
1945                    prompt_capabilities.clone(),
1946                    Default::default(),
1947                    Default::default(),
1948                    Default::default(),
1949                    "Test Agent".into(),
1950                    "Test",
1951                    EditorMode::AutoHeight {
1952                        max_lines: None,
1953                        min_lines: 1,
1954                    },
1955                    window,
1956                    cx,
1957                )
1958            });
1959            workspace.active_pane().update(cx, |pane, cx| {
1960                pane.add_item(
1961                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1962                    true,
1963                    true,
1964                    None,
1965                    window,
1966                    cx,
1967                );
1968            });
1969            message_editor.read(cx).focus_handle(cx).focus(window, cx);
1970            let editor = message_editor.read(cx).editor().clone();
1971            (message_editor, editor)
1972        });
1973
1974        cx.simulate_input("Lorem @");
1975
1976        editor.update_in(&mut cx, |editor, window, cx| {
1977            assert_eq!(editor.text(cx), "Lorem @");
1978            assert!(editor.has_visible_completions_menu());
1979
1980            assert_eq!(
1981                current_completion_labels(editor),
1982                &[
1983                    format!("eight.txt b{slash}"),
1984                    format!("seven.txt b{slash}"),
1985                    format!("six.txt b{slash}"),
1986                    format!("five.txt b{slash}"),
1987                    "Files & Directories".into(),
1988                    "Symbols".into()
1989                ]
1990            );
1991            editor.set_text("", window, cx);
1992        });
1993
1994        prompt_capabilities.replace(
1995            acp::PromptCapabilities::new()
1996                .image(true)
1997                .audio(true)
1998                .embedded_context(true),
1999        );
2000
2001        cx.simulate_input("Lorem ");
2002
2003        editor.update(&mut cx, |editor, cx| {
2004            assert_eq!(editor.text(cx), "Lorem ");
2005            assert!(!editor.has_visible_completions_menu());
2006        });
2007
2008        cx.simulate_input("@");
2009
2010        editor.update(&mut cx, |editor, cx| {
2011            assert_eq!(editor.text(cx), "Lorem @");
2012            assert!(editor.has_visible_completions_menu());
2013            assert_eq!(
2014                current_completion_labels(editor),
2015                &[
2016                    format!("eight.txt b{slash}"),
2017                    format!("seven.txt b{slash}"),
2018                    format!("six.txt b{slash}"),
2019                    format!("five.txt b{slash}"),
2020                    "Files & Directories".into(),
2021                    "Symbols".into(),
2022                    "Threads".into(),
2023                    "Fetch".into()
2024                ]
2025            );
2026        });
2027
2028        // Select and confirm "File"
2029        editor.update_in(&mut cx, |editor, window, cx| {
2030            assert!(editor.has_visible_completions_menu());
2031            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2032            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2033            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2034            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2035            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2036        });
2037
2038        cx.run_until_parked();
2039
2040        editor.update(&mut cx, |editor, cx| {
2041            assert_eq!(editor.text(cx), "Lorem @file ");
2042            assert!(editor.has_visible_completions_menu());
2043        });
2044
2045        cx.simulate_input("one");
2046
2047        editor.update(&mut cx, |editor, cx| {
2048            assert_eq!(editor.text(cx), "Lorem @file one");
2049            assert!(editor.has_visible_completions_menu());
2050            assert_eq!(
2051                current_completion_labels(editor),
2052                vec![format!("one.txt a{slash}")]
2053            );
2054        });
2055
2056        editor.update_in(&mut cx, |editor, window, cx| {
2057            assert!(editor.has_visible_completions_menu());
2058            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2059        });
2060
2061        let url_one = MentionUri::File {
2062            abs_path: path!("/dir/a/one.txt").into(),
2063        }
2064        .to_uri()
2065        .to_string();
2066        editor.update(&mut cx, |editor, cx| {
2067            let text = editor.text(cx);
2068            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2069            assert!(!editor.has_visible_completions_menu());
2070            assert_eq!(fold_ranges(editor, cx).len(), 1);
2071        });
2072
2073        let contents = message_editor
2074            .update(&mut cx, |message_editor, cx| {
2075                message_editor
2076                    .mention_set()
2077                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2078            })
2079            .await
2080            .unwrap()
2081            .into_values()
2082            .collect::<Vec<_>>();
2083
2084        {
2085            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2086                panic!("Unexpected mentions");
2087            };
2088            pretty_assertions::assert_eq!(content, "1");
2089            pretty_assertions::assert_eq!(
2090                uri,
2091                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2092            );
2093        }
2094
2095        cx.simulate_input(" ");
2096
2097        editor.update(&mut cx, |editor, cx| {
2098            let text = editor.text(cx);
2099            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2100            assert!(!editor.has_visible_completions_menu());
2101            assert_eq!(fold_ranges(editor, cx).len(), 1);
2102        });
2103
2104        cx.simulate_input("Ipsum ");
2105
2106        editor.update(&mut cx, |editor, cx| {
2107            let text = editor.text(cx);
2108            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2109            assert!(!editor.has_visible_completions_menu());
2110            assert_eq!(fold_ranges(editor, cx).len(), 1);
2111        });
2112
2113        cx.simulate_input("@file ");
2114
2115        editor.update(&mut cx, |editor, cx| {
2116            let text = editor.text(cx);
2117            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2118            assert!(editor.has_visible_completions_menu());
2119            assert_eq!(fold_ranges(editor, cx).len(), 1);
2120        });
2121
2122        editor.update_in(&mut cx, |editor, window, cx| {
2123            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2124        });
2125
2126        cx.run_until_parked();
2127
2128        let contents = message_editor
2129            .update(&mut cx, |message_editor, cx| {
2130                message_editor
2131                    .mention_set()
2132                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2133            })
2134            .await
2135            .unwrap()
2136            .into_values()
2137            .collect::<Vec<_>>();
2138
2139        let url_eight = MentionUri::File {
2140            abs_path: path!("/dir/b/eight.txt").into(),
2141        }
2142        .to_uri()
2143        .to_string();
2144
2145        {
2146            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2147                panic!("Unexpected mentions");
2148            };
2149            pretty_assertions::assert_eq!(content, "8");
2150            pretty_assertions::assert_eq!(
2151                uri,
2152                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2153            );
2154        }
2155
2156        editor.update(&mut cx, |editor, cx| {
2157            assert_eq!(
2158                editor.text(cx),
2159                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2160            );
2161            assert!(!editor.has_visible_completions_menu());
2162            assert_eq!(fold_ranges(editor, cx).len(), 2);
2163        });
2164
2165        let plain_text_language = Arc::new(language::Language::new(
2166            language::LanguageConfig {
2167                name: "Plain Text".into(),
2168                matcher: language::LanguageMatcher {
2169                    path_suffixes: vec!["txt".to_string()],
2170                    ..Default::default()
2171                },
2172                ..Default::default()
2173            },
2174            None,
2175        ));
2176
2177        // Register the language and fake LSP
2178        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2179        language_registry.add(plain_text_language);
2180
2181        let mut fake_language_servers = language_registry.register_fake_lsp(
2182            "Plain Text",
2183            language::FakeLspAdapter {
2184                capabilities: lsp::ServerCapabilities {
2185                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2186                    ..Default::default()
2187                },
2188                ..Default::default()
2189            },
2190        );
2191
2192        // Open the buffer to trigger LSP initialization
2193        let buffer = project
2194            .update(&mut cx, |project, cx| {
2195                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2196            })
2197            .await
2198            .unwrap();
2199
2200        // Register the buffer with language servers
2201        let _handle = project.update(&mut cx, |project, cx| {
2202            project.register_buffer_with_language_servers(&buffer, cx)
2203        });
2204
2205        cx.run_until_parked();
2206
2207        let fake_language_server = fake_language_servers.next().await.unwrap();
2208        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2209            move |_, _| async move {
2210                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2211                    #[allow(deprecated)]
2212                    lsp::SymbolInformation {
2213                        name: "MySymbol".into(),
2214                        location: lsp::Location {
2215                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2216                            range: lsp::Range::new(
2217                                lsp::Position::new(0, 0),
2218                                lsp::Position::new(0, 1),
2219                            ),
2220                        },
2221                        kind: lsp::SymbolKind::CONSTANT,
2222                        tags: None,
2223                        container_name: None,
2224                        deprecated: None,
2225                    },
2226                ])))
2227            },
2228        );
2229
2230        cx.simulate_input("@symbol ");
2231
2232        editor.update(&mut cx, |editor, cx| {
2233            assert_eq!(
2234                editor.text(cx),
2235                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2236            );
2237            assert!(editor.has_visible_completions_menu());
2238            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2239        });
2240
2241        editor.update_in(&mut cx, |editor, window, cx| {
2242            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2243        });
2244
2245        let symbol = MentionUri::Symbol {
2246            abs_path: path!("/dir/a/one.txt").into(),
2247            name: "MySymbol".into(),
2248            line_range: 0..=0,
2249        };
2250
2251        let contents = message_editor
2252            .update(&mut cx, |message_editor, cx| {
2253                message_editor
2254                    .mention_set()
2255                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2256            })
2257            .await
2258            .unwrap()
2259            .into_values()
2260            .collect::<Vec<_>>();
2261
2262        {
2263            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2264                panic!("Unexpected mentions");
2265            };
2266            pretty_assertions::assert_eq!(content, "1");
2267            pretty_assertions::assert_eq!(uri, &symbol);
2268        }
2269
2270        cx.run_until_parked();
2271
2272        editor.read_with(&cx, |editor, cx| {
2273            assert_eq!(
2274                editor.text(cx),
2275                format!(
2276                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2277                    symbol.to_uri(),
2278                )
2279            );
2280        });
2281
2282        // Try to mention an "image" file that will fail to load
2283        cx.simulate_input("@file x.png");
2284
2285        editor.update(&mut cx, |editor, cx| {
2286            assert_eq!(
2287                editor.text(cx),
2288                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2289            );
2290            assert!(editor.has_visible_completions_menu());
2291            assert_eq!(current_completion_labels(editor), &["x.png "]);
2292        });
2293
2294        editor.update_in(&mut cx, |editor, window, cx| {
2295            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2296        });
2297
2298        // Getting the message contents fails
2299        message_editor
2300            .update(&mut cx, |message_editor, cx| {
2301                message_editor
2302                    .mention_set()
2303                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2304            })
2305            .await
2306            .expect_err("Should fail to load x.png");
2307
2308        cx.run_until_parked();
2309
2310        // Mention was removed
2311        editor.read_with(&cx, |editor, cx| {
2312            assert_eq!(
2313                editor.text(cx),
2314                format!(
2315                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2316                    symbol.to_uri()
2317                )
2318            );
2319        });
2320
2321        // Once more
2322        cx.simulate_input("@file x.png");
2323
2324        editor.update(&mut cx, |editor, cx| {
2325                    assert_eq!(
2326                        editor.text(cx),
2327                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2328                    );
2329                    assert!(editor.has_visible_completions_menu());
2330                    assert_eq!(current_completion_labels(editor), &["x.png "]);
2331                });
2332
2333        editor.update_in(&mut cx, |editor, window, cx| {
2334            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2335        });
2336
2337        // This time don't immediately get the contents, just let the confirmed completion settle
2338        cx.run_until_parked();
2339
2340        // Mention was removed
2341        editor.read_with(&cx, |editor, cx| {
2342            assert_eq!(
2343                editor.text(cx),
2344                format!(
2345                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2346                    symbol.to_uri()
2347                )
2348            );
2349        });
2350
2351        // Now getting the contents succeeds, because the invalid mention was removed
2352        let contents = message_editor
2353            .update(&mut cx, |message_editor, cx| {
2354                message_editor
2355                    .mention_set()
2356                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2357            })
2358            .await
2359            .unwrap();
2360        assert_eq!(contents.len(), 3);
2361    }
2362
2363    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2364        let snapshot = editor.buffer().read(cx).snapshot(cx);
2365        editor.display_map.update(cx, |display_map, cx| {
2366            display_map
2367                .snapshot(cx)
2368                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
2369                .map(|fold| fold.range.to_point(&snapshot))
2370                .collect()
2371        })
2372    }
2373
2374    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2375        let completions = editor.current_completions().expect("Missing completions");
2376        completions
2377            .into_iter()
2378            .map(|completion| completion.label.text)
2379            .collect::<Vec<_>>()
2380    }
2381
2382    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2383        let completions = editor.current_completions().expect("Missing completions");
2384        completions
2385            .into_iter()
2386            .map(|completion| {
2387                (
2388                    completion.label.text,
2389                    completion
2390                        .documentation
2391                        .map(|d| d.text().to_string())
2392                        .unwrap_or_default(),
2393                )
2394            })
2395            .collect::<Vec<_>>()
2396    }
2397
2398    #[gpui::test]
2399    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
2400        init_test(cx);
2401
2402        let fs = FakeFs::new(cx.executor());
2403
2404        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2405        // Using plain text without a configured language, so no outline is available
2406        const LINE: &str = "This is a line of text in the file\n";
2407        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2408        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2409
2410        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2411        let small_content = "fn small_function() { /* small */ }\n";
2412        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2413
2414        fs.insert_tree(
2415            "/project",
2416            json!({
2417                "large_file.txt": large_content.clone(),
2418                "small_file.txt": small_content,
2419            }),
2420        )
2421        .await;
2422
2423        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2424
2425        let (workspace, cx) =
2426            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2427
2428        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2429        let history = cx
2430            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2431
2432        let message_editor = cx.update(|window, cx| {
2433            cx.new(|cx| {
2434                let editor = MessageEditor::new_with_cache(
2435                    workspace.downgrade(),
2436                    project.downgrade(),
2437                    thread_store.clone(),
2438                    history.downgrade(),
2439                    None,
2440                    Default::default(),
2441                    Default::default(),
2442                    Default::default(),
2443                    Default::default(),
2444                    "Test Agent".into(),
2445                    "Test",
2446                    EditorMode::AutoHeight {
2447                        min_lines: 1,
2448                        max_lines: None,
2449                    },
2450                    window,
2451                    cx,
2452                );
2453                // Enable embedded context so files are actually included
2454                editor
2455                    .prompt_capabilities
2456                    .replace(acp::PromptCapabilities::new().embedded_context(true));
2457                editor
2458            })
2459        });
2460
2461        // Test large file mention
2462        // Get the absolute path using the project's worktree
2463        let large_file_abs_path = project.read_with(cx, |project, cx| {
2464            let worktree = project.worktrees(cx).next().unwrap();
2465            let worktree_root = worktree.read(cx).abs_path();
2466            worktree_root.join("large_file.txt")
2467        });
2468        let large_file_task = message_editor.update(cx, |editor, cx| {
2469            editor.mention_set().update(cx, |set, cx| {
2470                set.confirm_mention_for_file(large_file_abs_path, true, cx)
2471            })
2472        });
2473
2474        let large_file_mention = large_file_task.await.unwrap();
2475        match large_file_mention {
2476            Mention::Text { content, .. } => {
2477                // Should contain some of the content but not all of it
2478                assert!(
2479                    content.contains(LINE),
2480                    "Should contain some of the file content"
2481                );
2482                assert!(
2483                    !content.contains(&LINE.repeat(100)),
2484                    "Should not contain the full file"
2485                );
2486                // Should be much smaller than original
2487                assert!(
2488                    content.len() < large_content.len() / 10,
2489                    "Should be significantly truncated"
2490                );
2491            }
2492            _ => panic!("Expected Text mention for large file"),
2493        }
2494
2495        // Test small file mention
2496        // Get the absolute path using the project's worktree
2497        let small_file_abs_path = project.read_with(cx, |project, cx| {
2498            let worktree = project.worktrees(cx).next().unwrap();
2499            let worktree_root = worktree.read(cx).abs_path();
2500            worktree_root.join("small_file.txt")
2501        });
2502        let small_file_task = message_editor.update(cx, |editor, cx| {
2503            editor.mention_set().update(cx, |set, cx| {
2504                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2505            })
2506        });
2507
2508        let small_file_mention = small_file_task.await.unwrap();
2509        match small_file_mention {
2510            Mention::Text { content, .. } => {
2511                // Should contain the full actual content
2512                assert_eq!(content, small_content);
2513            }
2514            _ => panic!("Expected Text mention for small file"),
2515        }
2516    }
2517
2518    #[gpui::test]
2519    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2520        init_test(cx);
2521        cx.update(LanguageModelRegistry::test);
2522
2523        let fs = FakeFs::new(cx.executor());
2524        fs.insert_tree("/project", json!({"file": ""})).await;
2525        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2526
2527        let (workspace, cx) =
2528            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2529
2530        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2531        let history = cx
2532            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2533
2534        // Create a thread metadata to insert as summary
2535        let thread_metadata = AgentSessionInfo {
2536            session_id: acp::SessionId::new("thread-123"),
2537            cwd: None,
2538            title: Some("Previous Conversation".into()),
2539            updated_at: Some(chrono::Utc::now()),
2540            meta: None,
2541        };
2542
2543        let message_editor = cx.update(|window, cx| {
2544            cx.new(|cx| {
2545                let mut editor = MessageEditor::new_with_cache(
2546                    workspace.downgrade(),
2547                    project.downgrade(),
2548                    thread_store.clone(),
2549                    history.downgrade(),
2550                    None,
2551                    Default::default(),
2552                    Default::default(),
2553                    Default::default(),
2554                    Default::default(),
2555                    "Test Agent".into(),
2556                    "Test",
2557                    EditorMode::AutoHeight {
2558                        min_lines: 1,
2559                        max_lines: None,
2560                    },
2561                    window,
2562                    cx,
2563                );
2564                editor.insert_thread_summary(thread_metadata.clone(), window, cx);
2565                editor
2566            })
2567        });
2568
2569        // Construct expected values for verification
2570        let expected_uri = MentionUri::Thread {
2571            id: thread_metadata.session_id.clone(),
2572            name: thread_metadata.title.as_ref().unwrap().to_string(),
2573        };
2574        let expected_title = thread_metadata.title.as_ref().unwrap();
2575        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
2576
2577        message_editor.read_with(cx, |editor, cx| {
2578            let text = editor.text(cx);
2579
2580            assert!(
2581                text.contains(&expected_link),
2582                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2583                expected_link,
2584                text
2585            );
2586
2587            let mentions = editor.mention_set().read(cx).mentions();
2588            assert_eq!(
2589                mentions.len(),
2590                1,
2591                "Expected exactly one mention after inserting thread summary"
2592            );
2593
2594            assert!(
2595                mentions.contains(&expected_uri),
2596                "Expected mentions to contain the thread URI"
2597            );
2598        });
2599    }
2600
2601    #[gpui::test]
2602    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
2603        init_test(cx);
2604        cx.update(LanguageModelRegistry::test);
2605
2606        let fs = FakeFs::new(cx.executor());
2607        fs.insert_tree("/project", json!({"file": ""})).await;
2608        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2609
2610        let (workspace, cx) =
2611            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2612
2613        let thread_store = None;
2614        let history = cx
2615            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2616
2617        let thread_metadata = AgentSessionInfo {
2618            session_id: acp::SessionId::new("thread-123"),
2619            cwd: None,
2620            title: Some("Previous Conversation".into()),
2621            updated_at: Some(chrono::Utc::now()),
2622            meta: None,
2623        };
2624
2625        let message_editor = cx.update(|window, cx| {
2626            cx.new(|cx| {
2627                let mut editor = MessageEditor::new_with_cache(
2628                    workspace.downgrade(),
2629                    project.downgrade(),
2630                    thread_store.clone(),
2631                    history.downgrade(),
2632                    None,
2633                    Default::default(),
2634                    Default::default(),
2635                    Default::default(),
2636                    Default::default(),
2637                    "Test Agent".into(),
2638                    "Test",
2639                    EditorMode::AutoHeight {
2640                        min_lines: 1,
2641                        max_lines: None,
2642                    },
2643                    window,
2644                    cx,
2645                );
2646                editor.insert_thread_summary(thread_metadata, window, cx);
2647                editor
2648            })
2649        });
2650
2651        message_editor.read_with(cx, |editor, cx| {
2652            assert!(
2653                editor.text(cx).is_empty(),
2654                "Expected thread summary to be skipped for external agents"
2655            );
2656            assert!(
2657                editor.mention_set().read(cx).mentions().is_empty(),
2658                "Expected no mentions when thread summary is skipped"
2659            );
2660        });
2661    }
2662
2663    #[gpui::test]
2664    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
2665        init_test(cx);
2666
2667        let fs = FakeFs::new(cx.executor());
2668        fs.insert_tree("/project", json!({"file": ""})).await;
2669        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2670
2671        let (workspace, cx) =
2672            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2673
2674        let thread_store = None;
2675        let history = cx
2676            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2677
2678        let message_editor = cx.update(|window, cx| {
2679            cx.new(|cx| {
2680                MessageEditor::new_with_cache(
2681                    workspace.downgrade(),
2682                    project.downgrade(),
2683                    thread_store.clone(),
2684                    history.downgrade(),
2685                    None,
2686                    Default::default(),
2687                    Default::default(),
2688                    Default::default(),
2689                    Default::default(),
2690                    "Test Agent".into(),
2691                    "Test",
2692                    EditorMode::AutoHeight {
2693                        min_lines: 1,
2694                        max_lines: None,
2695                    },
2696                    window,
2697                    cx,
2698                )
2699            })
2700        });
2701
2702        message_editor.update(cx, |editor, _cx| {
2703            editor
2704                .prompt_capabilities
2705                .replace(acp::PromptCapabilities::new().embedded_context(true));
2706        });
2707
2708        let supported_modes = {
2709            let app = cx.app.borrow();
2710            message_editor.supported_modes(&app)
2711        };
2712
2713        assert!(
2714            !supported_modes.contains(&PromptContextType::Thread),
2715            "Expected thread mode to be hidden when thread mentions are disabled"
2716        );
2717    }
2718
2719    #[gpui::test]
2720    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
2721        init_test(cx);
2722
2723        let fs = FakeFs::new(cx.executor());
2724        fs.insert_tree("/project", json!({"file": ""})).await;
2725        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2726
2727        let (workspace, cx) =
2728            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2729
2730        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2731        let history = cx
2732            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2733
2734        let message_editor = cx.update(|window, cx| {
2735            cx.new(|cx| {
2736                MessageEditor::new_with_cache(
2737                    workspace.downgrade(),
2738                    project.downgrade(),
2739                    thread_store.clone(),
2740                    history.downgrade(),
2741                    None,
2742                    Default::default(),
2743                    Default::default(),
2744                    Default::default(),
2745                    Default::default(),
2746                    "Test Agent".into(),
2747                    "Test",
2748                    EditorMode::AutoHeight {
2749                        min_lines: 1,
2750                        max_lines: None,
2751                    },
2752                    window,
2753                    cx,
2754                )
2755            })
2756        });
2757
2758        message_editor.update(cx, |editor, _cx| {
2759            editor
2760                .prompt_capabilities
2761                .replace(acp::PromptCapabilities::new().embedded_context(true));
2762        });
2763
2764        let supported_modes = {
2765            let app = cx.app.borrow();
2766            message_editor.supported_modes(&app)
2767        };
2768
2769        assert!(
2770            supported_modes.contains(&PromptContextType::Thread),
2771            "Expected thread mode to be visible when enabled"
2772        );
2773    }
2774
2775    #[gpui::test]
2776    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
2777        init_test(cx);
2778
2779        let fs = FakeFs::new(cx.executor());
2780        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
2781            .await;
2782        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2783
2784        let (workspace, cx) =
2785            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2786
2787        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2788        let history = cx
2789            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2790
2791        let message_editor = cx.update(|window, cx| {
2792            cx.new(|cx| {
2793                MessageEditor::new_with_cache(
2794                    workspace.downgrade(),
2795                    project.downgrade(),
2796                    thread_store.clone(),
2797                    history.downgrade(),
2798                    None,
2799                    Default::default(),
2800                    Default::default(),
2801                    Default::default(),
2802                    Default::default(),
2803                    "Test Agent".into(),
2804                    "Test",
2805                    EditorMode::AutoHeight {
2806                        min_lines: 1,
2807                        max_lines: None,
2808                    },
2809                    window,
2810                    cx,
2811                )
2812            })
2813        });
2814        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2815
2816        cx.run_until_parked();
2817
2818        editor.update_in(cx, |editor, window, cx| {
2819            editor.set_text("  \u{A0}してhello world  ", window, cx);
2820        });
2821
2822        let (content, _) = message_editor
2823            .update(cx, |message_editor, cx| {
2824                message_editor.contents_with_cache(false, None, None, cx)
2825            })
2826            .await
2827            .unwrap();
2828
2829        assert_eq!(content, vec!["してhello world".into()]);
2830    }
2831
2832    #[gpui::test]
2833    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
2834        init_test(cx);
2835
2836        let fs = FakeFs::new(cx.executor());
2837
2838        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
2839
2840        fs.insert_tree(
2841            "/project",
2842            json!({
2843                "src": {
2844                    "main.rs": file_content,
2845                }
2846            }),
2847        )
2848        .await;
2849
2850        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2851
2852        let (workspace, cx) =
2853            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2854
2855        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2856        let history = cx
2857            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
2858
2859        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
2860            let workspace_handle = cx.weak_entity();
2861            let message_editor = cx.new(|cx| {
2862                MessageEditor::new_with_cache(
2863                    workspace_handle,
2864                    project.downgrade(),
2865                    thread_store.clone(),
2866                    history.downgrade(),
2867                    None,
2868                    Default::default(),
2869                    Default::default(),
2870                    Default::default(),
2871                    Default::default(),
2872                    "Test Agent".into(),
2873                    "Test",
2874                    EditorMode::AutoHeight {
2875                        max_lines: None,
2876                        min_lines: 1,
2877                    },
2878                    window,
2879                    cx,
2880                )
2881            });
2882            workspace.active_pane().update(cx, |pane, cx| {
2883                pane.add_item(
2884                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2885                    true,
2886                    true,
2887                    None,
2888                    window,
2889                    cx,
2890                );
2891            });
2892            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2893            let editor = message_editor.read(cx).editor().clone();
2894            (message_editor, editor)
2895        });
2896
2897        cx.simulate_input("What is in @file main");
2898
2899        editor.update_in(cx, |editor, window, cx| {
2900            assert!(editor.has_visible_completions_menu());
2901            assert_eq!(editor.text(cx), "What is in @file main");
2902            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2903        });
2904
2905        let content = message_editor
2906            .update(cx, |editor, cx| {
2907                editor.contents_with_cache(false, None, None, cx)
2908            })
2909            .await
2910            .unwrap()
2911            .0;
2912
2913        let main_rs_uri = if cfg!(windows) {
2914            "file:///C:/project/src/main.rs"
2915        } else {
2916            "file:///project/src/main.rs"
2917        };
2918
2919        // When embedded context is `false` we should get a resource link
2920        pretty_assertions::assert_eq!(
2921            content,
2922            vec![
2923                "What is in ".into(),
2924                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
2925            ]
2926        );
2927
2928        message_editor.update(cx, |editor, _cx| {
2929            editor
2930                .prompt_capabilities
2931                .replace(acp::PromptCapabilities::new().embedded_context(true))
2932        });
2933
2934        let content = message_editor
2935            .update(cx, |editor, cx| {
2936                editor.contents_with_cache(false, None, None, cx)
2937            })
2938            .await
2939            .unwrap()
2940            .0;
2941
2942        // When embedded context is `true` we should get a resource
2943        pretty_assertions::assert_eq!(
2944            content,
2945            vec![
2946                "What is in ".into(),
2947                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
2948                    acp::EmbeddedResourceResource::TextResourceContents(
2949                        acp::TextResourceContents::new(file_content, main_rs_uri)
2950                    )
2951                ))
2952            ]
2953        );
2954    }
2955
2956    #[gpui::test]
2957    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
2958        init_test(cx);
2959
2960        let app_state = cx.update(AppState::test);
2961
2962        cx.update(|cx| {
2963            editor::init(cx);
2964            workspace::init(app_state.clone(), cx);
2965        });
2966
2967        app_state
2968            .fs
2969            .as_fake()
2970            .insert_tree(
2971                path!("/dir"),
2972                json!({
2973                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
2974                }),
2975            )
2976            .await;
2977
2978        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2979        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2980        let workspace = window.root(cx).unwrap();
2981
2982        let worktree = project.update(cx, |project, cx| {
2983            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2984            assert_eq!(worktrees.len(), 1);
2985            worktrees.pop().unwrap()
2986        });
2987        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2988
2989        let mut cx = VisualTestContext::from_window(*window, cx);
2990
2991        // Open a regular editor with the created file, and select a portion of
2992        // the text that will be used for the selections that are meant to be
2993        // inserted in the agent panel.
2994        let editor = workspace
2995            .update_in(&mut cx, |workspace, window, cx| {
2996                workspace.open_path(
2997                    ProjectPath {
2998                        worktree_id,
2999                        path: rel_path("test.txt").into(),
3000                    },
3001                    None,
3002                    false,
3003                    window,
3004                    cx,
3005                )
3006            })
3007            .await
3008            .unwrap()
3009            .downcast::<Editor>()
3010            .unwrap();
3011
3012        editor.update_in(&mut cx, |editor, window, cx| {
3013            editor.change_selections(Default::default(), window, cx, |selections| {
3014                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
3015            });
3016        });
3017
3018        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3019        let history = cx
3020            .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
3021
3022        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
3023        // to ensure we have a fixed viewport, so we can eventually actually
3024        // place the cursor outside of the visible area.
3025        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
3026            let workspace_handle = cx.weak_entity();
3027            let message_editor = cx.new(|cx| {
3028                MessageEditor::new_with_cache(
3029                    workspace_handle,
3030                    project.downgrade(),
3031                    thread_store.clone(),
3032                    history.downgrade(),
3033                    None,
3034                    Default::default(),
3035                    Default::default(),
3036                    Default::default(),
3037                    Default::default(),
3038                    "Test Agent".into(),
3039                    "Test",
3040                    EditorMode::full(),
3041                    window,
3042                    cx,
3043                )
3044            });
3045            workspace.active_pane().update(cx, |pane, cx| {
3046                pane.add_item(
3047                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3048                    true,
3049                    true,
3050                    None,
3051                    window,
3052                    cx,
3053                );
3054            });
3055
3056            message_editor
3057        });
3058
3059        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3060            message_editor.editor.update(cx, |editor, cx| {
3061                // Update the Agent Panel's Message Editor text to have 100
3062                // lines, ensuring that the cursor is set at line 90 and that we
3063                // then scroll all the way to the top, so the cursor's position
3064                // remains off screen.
3065                let mut lines = String::new();
3066                for _ in 1..=100 {
3067                    lines.push_str(&"Another line in the agent panel's message editor\n");
3068                }
3069                editor.set_text(lines.as_str(), window, cx);
3070                editor.change_selections(Default::default(), window, cx, |selections| {
3071                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
3072                });
3073                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
3074            });
3075        });
3076
3077        cx.run_until_parked();
3078
3079        // Before proceeding, let's assert that the cursor is indeed off screen,
3080        // otherwise the rest of the test doesn't make sense.
3081        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3082            message_editor.editor.update(cx, |editor, cx| {
3083                let snapshot = editor.snapshot(window, cx);
3084                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3085                let scroll_top = snapshot.scroll_position().y as u32;
3086                let visible_lines = editor.visible_line_count().unwrap() as u32;
3087                let visible_range = scroll_top..(scroll_top + visible_lines);
3088
3089                assert!(!visible_range.contains(&cursor_row));
3090            })
3091        });
3092
3093        // Now let's insert the selection in the Agent Panel's editor and
3094        // confirm that, after the insertion, the cursor is now in the visible
3095        // range.
3096        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3097            message_editor.insert_selections(window, cx);
3098        });
3099
3100        cx.run_until_parked();
3101
3102        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3103            message_editor.editor.update(cx, |editor, cx| {
3104                let snapshot = editor.snapshot(window, cx);
3105                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3106                let scroll_top = snapshot.scroll_position().y as u32;
3107                let visible_lines = editor.visible_line_count().unwrap() as u32;
3108                let visible_range = scroll_top..(scroll_top + visible_lines);
3109
3110                assert!(visible_range.contains(&cursor_row));
3111            })
3112        });
3113    }
3114}