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