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