message_editor.rs

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