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