message_editor.rs

   1use crate::{
   2    ChatWithFollow,
   3    completion_provider::{
   4        PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,
   5        PromptContextType, SlashCommandCompletion,
   6    },
   7    mention_set::{
   8        Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,
   9    },
  10};
  11use acp_thread::MentionUri;
  12use agent::HistoryStore;
  13use agent_client_protocol as acp;
  14use anyhow::{Result, anyhow};
  15use collections::HashSet;
  16use editor::{
  17    Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
  18    EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, ToOffset,
  19    actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
  20};
  21use futures::{FutureExt as _, future::join_all};
  22use gpui::{
  23    AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext,
  24    SharedString, Subscription, Task, TextStyle, WeakEntity,
  25};
  26use language::{Buffer, Language, language_settings::InlayHintKind};
  27use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
  28use prompt_store::PromptStore;
  29use rope::Point;
  30use settings::Settings;
  31use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
  32use theme::ThemeSettings;
  33use ui::prelude::*;
  34use util::{ResultExt, debug_panic};
  35use workspace::{CollaboratorId, Workspace};
  36use zed_actions::agent::Chat;
  37
  38pub struct MessageEditor {
  39    mention_set: Entity<MentionSet>,
  40    editor: Entity<Editor>,
  41    project: Entity<Project>,
  42    workspace: WeakEntity<Workspace>,
  43    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  44    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  45    agent_name: SharedString,
  46    _subscriptions: Vec<Subscription>,
  47    _parse_slash_command_task: Task<()>,
  48}
  49
  50#[derive(Clone, Copy, Debug)]
  51pub enum MessageEditorEvent {
  52    Send,
  53    Cancel,
  54    Focus,
  55    LostFocus,
  56}
  57
  58impl EventEmitter<MessageEditorEvent> for MessageEditor {}
  59
  60const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
  61
  62impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
  63    fn supports_images(&self, cx: &App) -> bool {
  64        self.read(cx).prompt_capabilities.borrow().image
  65    }
  66
  67    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
  68        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
  69        if self.read(cx).prompt_capabilities.borrow().embedded_context {
  70            supported.extend(&[
  71                PromptContextType::Thread,
  72                PromptContextType::Fetch,
  73                PromptContextType::Rules,
  74            ]);
  75        }
  76        supported
  77    }
  78
  79    fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
  80        self.read(cx)
  81            .available_commands
  82            .borrow()
  83            .iter()
  84            .map(|cmd| crate::completion_provider::AvailableCommand {
  85                name: cmd.name.clone().into(),
  86                description: cmd.description.clone().into(),
  87                requires_argument: cmd.input.is_some(),
  88            })
  89            .collect()
  90    }
  91
  92    fn confirm_command(&self, cx: &mut App) {
  93        self.update(cx, |this, cx| this.send(cx));
  94    }
  95}
  96
  97impl MessageEditor {
  98    pub fn new(
  99        workspace: WeakEntity<Workspace>,
 100        project: Entity<Project>,
 101        history_store: Entity<HistoryStore>,
 102        prompt_store: Option<Entity<PromptStore>>,
 103        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
 104        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 105        agent_name: SharedString,
 106        placeholder: &str,
 107        mode: EditorMode,
 108        window: &mut Window,
 109        cx: &mut Context<Self>,
 110    ) -> Self {
 111        let language = Language::new(
 112            language::LanguageConfig {
 113                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 114                ..Default::default()
 115            },
 116            None,
 117        );
 118
 119        let editor = cx.new(|cx| {
 120            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 121            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 122
 123            let mut editor = Editor::new(mode, buffer, None, window, cx);
 124            editor.set_placeholder_text(placeholder, window, cx);
 125            editor.set_show_indent_guides(false, cx);
 126            editor.set_soft_wrap();
 127            editor.set_use_modal_editing(true);
 128            editor.set_context_menu_options(ContextMenuOptions {
 129                min_entries_visible: 12,
 130                max_entries_visible: 12,
 131                placement: Some(ContextMenuPlacement::Above),
 132            });
 133            editor.register_addon(MessageEditorAddon::new());
 134            editor
 135        });
 136        let mention_set = cx.new(|cx| {
 137            MentionSet::new(
 138                editor.clone(),
 139                project.downgrade(),
 140                history_store.clone(),
 141                prompt_store.clone(),
 142                window,
 143                cx,
 144            )
 145        });
 146        let completion_provider = Rc::new(PromptCompletionProvider::new(
 147            cx.entity(),
 148            mention_set.clone(),
 149            history_store.clone(),
 150            prompt_store.clone(),
 151            workspace.clone(),
 152        ));
 153        editor.update(cx, |editor, _cx| {
 154            editor.set_completion_provider(Some(completion_provider.clone()))
 155        });
 156
 157        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 158            cx.emit(MessageEditorEvent::Focus)
 159        })
 160        .detach();
 161        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 162            cx.emit(MessageEditorEvent::LostFocus)
 163        })
 164        .detach();
 165
 166        let mut has_hint = false;
 167        let mut subscriptions = Vec::new();
 168
 169        subscriptions.push(cx.subscribe(&editor, {
 170            move |this, editor, event, cx| {
 171                if let EditorEvent::Edited { .. } = event
 172                    && !editor.read(cx).read_only(cx)
 173                {
 174                    editor.update(cx, |editor, cx| {
 175                        let new_hints = this
 176                            .command_hint(editor.buffer(), cx)
 177                            .into_iter()
 178                            .collect::<Vec<_>>();
 179                        let has_new_hint = !new_hints.is_empty();
 180                        editor.splice_inlays(
 181                            if has_hint {
 182                                &[COMMAND_HINT_INLAY_ID]
 183                            } else {
 184                                &[]
 185                            },
 186                            new_hints,
 187                            cx,
 188                        );
 189                        has_hint = has_new_hint;
 190                    });
 191                    cx.notify();
 192                }
 193            }
 194        }));
 195
 196        Self {
 197            editor,
 198            project,
 199            mention_set,
 200            workspace,
 201            prompt_capabilities,
 202            available_commands,
 203            agent_name,
 204            _subscriptions: subscriptions,
 205            _parse_slash_command_task: Task::ready(()),
 206        }
 207    }
 208
 209    fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
 210        let available_commands = self.available_commands.borrow();
 211        if available_commands.is_empty() {
 212            return None;
 213        }
 214
 215        let snapshot = buffer.read(cx).snapshot(cx);
 216        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 217        if parsed_command.argument.is_some() {
 218            return None;
 219        }
 220
 221        let command_name = parsed_command.command?;
 222        let available_command = available_commands
 223            .iter()
 224            .find(|command| command.name == command_name)?;
 225
 226        let acp::AvailableCommandInput::Unstructured { mut hint } =
 227            available_command.input.clone()?;
 228
 229        let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
 230        if hint_pos > snapshot.len() {
 231            hint_pos = snapshot.len();
 232            hint.insert(0, ' ');
 233        }
 234
 235        let hint_pos = snapshot.anchor_after(hint_pos);
 236
 237        Some(Inlay::hint(
 238            COMMAND_HINT_INLAY_ID,
 239            hint_pos,
 240            &InlayHint {
 241                position: hint_pos.text_anchor,
 242                label: InlayHintLabel::String(hint),
 243                kind: Some(InlayHintKind::Parameter),
 244                padding_left: false,
 245                padding_right: false,
 246                tooltip: None,
 247                resolve_state: project::ResolveState::Resolved,
 248            },
 249        ))
 250    }
 251
 252    pub fn insert_thread_summary(
 253        &mut self,
 254        thread: agent::DbThreadMetadata,
 255        window: &mut Window,
 256        cx: &mut Context<Self>,
 257    ) {
 258        let Some(workspace) = self.workspace.upgrade() else {
 259            return;
 260        };
 261        let uri = MentionUri::Thread {
 262            id: thread.id.clone(),
 263            name: thread.title.to_string(),
 264        };
 265        let content = format!("{}\n", uri.as_link());
 266
 267        let content_len = content.len() - 1;
 268
 269        let start = self.editor.update(cx, |editor, cx| {
 270            editor.set_text(content, window, cx);
 271            editor
 272                .buffer()
 273                .read(cx)
 274                .snapshot(cx)
 275                .anchor_before(Point::zero())
 276                .text_anchor
 277        });
 278
 279        let supports_images = self.prompt_capabilities.borrow().image;
 280
 281        self.mention_set
 282            .update(cx, |mention_set, cx| {
 283                mention_set.confirm_mention_completion(
 284                    thread.title,
 285                    start,
 286                    content_len,
 287                    uri,
 288                    supports_images,
 289                    &workspace,
 290                    window,
 291                    cx,
 292                )
 293            })
 294            .detach();
 295    }
 296
 297    #[cfg(test)]
 298    pub(crate) fn editor(&self) -> &Entity<Editor> {
 299        &self.editor
 300    }
 301
 302    pub fn is_empty(&self, cx: &App) -> bool {
 303        self.editor.read(cx).is_empty(cx)
 304    }
 305
 306    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 307        self.editor
 308            .read(cx)
 309            .context_menu()
 310            .borrow()
 311            .as_ref()
 312            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 313    }
 314
 315    #[cfg(test)]
 316    pub fn mention_set(&self) -> &Entity<MentionSet> {
 317        &self.mention_set
 318    }
 319
 320    fn validate_slash_commands(
 321        text: &str,
 322        available_commands: &[acp::AvailableCommand],
 323        agent_name: &str,
 324    ) -> Result<()> {
 325        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 326            if let Some(command_name) = parsed_command.command {
 327                // Check if this command is in the list of available commands from the server
 328                let is_supported = available_commands
 329                    .iter()
 330                    .any(|cmd| cmd.name == command_name);
 331
 332                if !is_supported {
 333                    return Err(anyhow!(
 334                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 335                        command_name,
 336                        agent_name,
 337                        if available_commands.is_empty() {
 338                            "none".to_string()
 339                        } else {
 340                            available_commands
 341                                .iter()
 342                                .map(|cmd| format!("/{}", cmd.name))
 343                                .collect::<Vec<_>>()
 344                                .join(", ")
 345                        }
 346                    ));
 347                }
 348            }
 349        }
 350        Ok(())
 351    }
 352
 353    pub fn contents(
 354        &self,
 355        full_mention_content: bool,
 356        cx: &mut Context<Self>,
 357    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 358        // Check for unsupported slash commands before spawning async task
 359        let text = self.editor.read(cx).text(cx);
 360        let available_commands = self.available_commands.borrow().clone();
 361        if let Err(err) =
 362            Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
 363        {
 364            return Task::ready(Err(err));
 365        }
 366
 367        let contents = self
 368            .mention_set
 369            .update(cx, |store, cx| store.contents(full_mention_content, cx));
 370        let editor = self.editor.clone();
 371        let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
 372
 373        cx.spawn(async move |_, cx| {
 374            let contents = contents.await?;
 375            let mut all_tracked_buffers = Vec::new();
 376
 377            let result = editor.update(cx, |editor, cx| {
 378                let (mut ix, _) = text
 379                    .char_indices()
 380                    .find(|(_, c)| !c.is_whitespace())
 381                    .unwrap_or((0, '\0'));
 382                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 383                let text = editor.text(cx);
 384                editor.display_map.update(cx, |map, cx| {
 385                    let snapshot = map.snapshot(cx);
 386                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 387                        let Some((uri, mention)) = contents.get(&crease_id) else {
 388                            continue;
 389                        };
 390
 391                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
 392                        if crease_range.start.0 > ix {
 393                            let chunk = text[ix..crease_range.start.0].into();
 394                            chunks.push(chunk);
 395                        }
 396                        let chunk = match mention {
 397                            Mention::Text {
 398                                content,
 399                                tracked_buffers,
 400                            } => {
 401                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 402                                if supports_embedded_context {
 403                                    acp::ContentBlock::Resource(acp::EmbeddedResource {
 404                                        annotations: None,
 405                                        resource:
 406                                            acp::EmbeddedResourceResource::TextResourceContents(
 407                                                acp::TextResourceContents {
 408                                                    mime_type: None,
 409                                                    text: content.clone(),
 410                                                    uri: uri.to_uri().to_string(),
 411                                                    meta: None,
 412                                                },
 413                                            ),
 414                                        meta: None,
 415                                    })
 416                                } else {
 417                                    acp::ContentBlock::ResourceLink(acp::ResourceLink {
 418                                        name: uri.name(),
 419                                        uri: uri.to_uri().to_string(),
 420                                        annotations: None,
 421                                        description: None,
 422                                        mime_type: None,
 423                                        size: None,
 424                                        title: None,
 425                                        meta: None,
 426                                    })
 427                                }
 428                            }
 429                            Mention::Image(mention_image) => {
 430                                let uri = match uri {
 431                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 432                                    MentionUri::PastedImage => None,
 433                                    other => {
 434                                        debug_panic!(
 435                                            "unexpected mention uri for image: {:?}",
 436                                            other
 437                                        );
 438                                        None
 439                                    }
 440                                };
 441                                acp::ContentBlock::Image(acp::ImageContent {
 442                                    annotations: None,
 443                                    data: mention_image.data.to_string(),
 444                                    mime_type: mention_image.format.mime_type().into(),
 445                                    uri,
 446                                    meta: None,
 447                                })
 448                            }
 449                            Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
 450                                name: uri.name(),
 451                                uri: uri.to_uri().to_string(),
 452                                annotations: None,
 453                                description: None,
 454                                mime_type: None,
 455                                size: None,
 456                                title: None,
 457                                meta: None,
 458                            }),
 459                        };
 460                        chunks.push(chunk);
 461                        ix = crease_range.end.0;
 462                    }
 463
 464                    if ix < text.len() {
 465                        let last_chunk = text[ix..].trim_end().to_owned();
 466                        if !last_chunk.is_empty() {
 467                            chunks.push(last_chunk.into());
 468                        }
 469                    }
 470                });
 471                Ok((chunks, all_tracked_buffers))
 472            })?;
 473            result
 474        })
 475    }
 476
 477    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 478        self.editor.update(cx, |editor, cx| {
 479            editor.clear(window, cx);
 480            editor.remove_creases(
 481                self.mention_set.update(cx, |mention_set, _cx| {
 482                    mention_set
 483                        .remove_all()
 484                        .map(|(crease_id, _)| crease_id)
 485                        .collect::<Vec<_>>()
 486                }),
 487                cx,
 488            )
 489        });
 490    }
 491
 492    pub fn send(&mut self, cx: &mut Context<Self>) {
 493        if self.is_empty(cx) {
 494            return;
 495        }
 496        self.editor.update(cx, |editor, cx| {
 497            editor.clear_inlay_hints(cx);
 498        });
 499        cx.emit(MessageEditorEvent::Send)
 500    }
 501
 502    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 503        let editor = self.editor.clone();
 504
 505        cx.spawn_in(window, async move |_, cx| {
 506            editor
 507                .update_in(cx, |editor, window, cx| {
 508                    let menu_is_open =
 509                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 510                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 511                        });
 512
 513                    let has_at_sign = {
 514                        let snapshot = editor.display_snapshot(cx);
 515                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 516                        let offset = cursor.to_offset(&snapshot);
 517                        if offset.0 > 0 {
 518                            snapshot
 519                                .buffer_snapshot()
 520                                .reversed_chars_at(offset)
 521                                .next()
 522                                .map(|sign| sign == '@')
 523                                .unwrap_or(false)
 524                        } else {
 525                            false
 526                        }
 527                    };
 528
 529                    if menu_is_open && has_at_sign {
 530                        return;
 531                    }
 532
 533                    editor.insert("@", window, cx);
 534                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 535                })
 536                .log_err();
 537        })
 538        .detach();
 539    }
 540
 541    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 542        self.send(cx);
 543    }
 544
 545    fn chat_with_follow(
 546        &mut self,
 547        _: &ChatWithFollow,
 548        window: &mut Window,
 549        cx: &mut Context<Self>,
 550    ) {
 551        self.workspace
 552            .update(cx, |this, cx| {
 553                this.follow(CollaboratorId::Agent, window, cx)
 554            })
 555            .log_err();
 556
 557        self.send(cx);
 558    }
 559
 560    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 561        cx.emit(MessageEditorEvent::Cancel)
 562    }
 563
 564    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 565        if self.prompt_capabilities.borrow().image
 566            && let Some(task) =
 567                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
 568        {
 569            task.detach();
 570        }
 571    }
 572
 573    pub fn insert_dragged_files(
 574        &mut self,
 575        paths: Vec<project::ProjectPath>,
 576        added_worktrees: Vec<Entity<Worktree>>,
 577        window: &mut Window,
 578        cx: &mut Context<Self>,
 579    ) {
 580        let Some(workspace) = self.workspace.upgrade() else {
 581            return;
 582        };
 583        let path_style = self.project.read(cx).path_style(cx);
 584        let buffer = self.editor.read(cx).buffer().clone();
 585        let Some(buffer) = buffer.read(cx).as_singleton() else {
 586            return;
 587        };
 588        let mut tasks = Vec::new();
 589        for path in paths {
 590            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
 591                continue;
 592            };
 593            let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 594                continue;
 595            };
 596            let abs_path = worktree.read(cx).absolutize(&path.path);
 597            let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 598                &path.path,
 599                worktree.read(cx).root_name(),
 600                path_style,
 601            );
 602
 603            let uri = if entry.is_dir() {
 604                MentionUri::Directory { abs_path }
 605            } else {
 606                MentionUri::File { abs_path }
 607            };
 608
 609            let new_text = format!("{} ", uri.as_link());
 610            let content_len = new_text.len() - 1;
 611
 612            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 613
 614            self.editor.update(cx, |message_editor, cx| {
 615                message_editor.edit(
 616                    [(
 617                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 618                        new_text,
 619                    )],
 620                    cx,
 621                );
 622            });
 623            let supports_images = self.prompt_capabilities.borrow().image;
 624            tasks.push(self.mention_set.update(cx, |mention_set, cx| {
 625                mention_set.confirm_mention_completion(
 626                    file_name,
 627                    anchor,
 628                    content_len,
 629                    uri,
 630                    supports_images,
 631                    &workspace,
 632                    window,
 633                    cx,
 634                )
 635            }));
 636        }
 637        cx.spawn(async move |_, _| {
 638            join_all(tasks).await;
 639            drop(added_worktrees);
 640        })
 641        .detach();
 642    }
 643
 644    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 645        let editor = self.editor.read(cx);
 646        let editor_buffer = editor.buffer().read(cx);
 647        let Some(buffer) = editor_buffer.as_singleton() else {
 648            return;
 649        };
 650        let cursor_anchor = editor.selections.newest_anchor().head();
 651        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
 652        let anchor = buffer.update(cx, |buffer, _cx| {
 653            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
 654        });
 655        let Some(workspace) = self.workspace.upgrade() else {
 656            return;
 657        };
 658        let Some(completion) =
 659            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
 660                PromptContextAction::AddSelections,
 661                anchor..anchor,
 662                self.mention_set.downgrade(),
 663                &workspace,
 664                cx,
 665            )
 666        else {
 667            return;
 668        };
 669
 670        self.editor.update(cx, |message_editor, cx| {
 671            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
 672            message_editor.request_autoscroll(Autoscroll::fit(), cx);
 673        });
 674        if let Some(confirm) = completion.confirm {
 675            confirm(CompletionIntent::Complete, window, cx);
 676        }
 677    }
 678
 679    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
 680        self.editor.update(cx, |message_editor, cx| {
 681            message_editor.set_read_only(read_only);
 682            cx.notify()
 683        })
 684    }
 685
 686    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
 687        self.editor.update(cx, |editor, cx| {
 688            editor.set_mode(mode);
 689            cx.notify()
 690        });
 691    }
 692
 693    pub fn set_message(
 694        &mut self,
 695        message: Vec<acp::ContentBlock>,
 696        window: &mut Window,
 697        cx: &mut Context<Self>,
 698    ) {
 699        self.clear(window, cx);
 700
 701        let path_style = self.project.read(cx).path_style(cx);
 702        let mut text = String::new();
 703        let mut mentions = Vec::new();
 704
 705        for chunk in message {
 706            match chunk {
 707                acp::ContentBlock::Text(text_content) => {
 708                    text.push_str(&text_content.text);
 709                }
 710                acp::ContentBlock::Resource(acp::EmbeddedResource {
 711                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
 712                    ..
 713                }) => {
 714                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
 715                    else {
 716                        continue;
 717                    };
 718                    let start = text.len();
 719                    write!(&mut text, "{}", mention_uri.as_link()).ok();
 720                    let end = text.len();
 721                    mentions.push((
 722                        start..end,
 723                        mention_uri,
 724                        Mention::Text {
 725                            content: resource.text,
 726                            tracked_buffers: Vec::new(),
 727                        },
 728                    ));
 729                }
 730                acp::ContentBlock::ResourceLink(resource) => {
 731                    if let Some(mention_uri) =
 732                        MentionUri::parse(&resource.uri, path_style).log_err()
 733                    {
 734                        let start = text.len();
 735                        write!(&mut text, "{}", mention_uri.as_link()).ok();
 736                        let end = text.len();
 737                        mentions.push((start..end, mention_uri, Mention::Link));
 738                    }
 739                }
 740                acp::ContentBlock::Image(acp::ImageContent {
 741                    uri,
 742                    data,
 743                    mime_type,
 744                    annotations: _,
 745                    meta: _,
 746                }) => {
 747                    let mention_uri = if let Some(uri) = uri {
 748                        MentionUri::parse(&uri, path_style)
 749                    } else {
 750                        Ok(MentionUri::PastedImage)
 751                    };
 752                    let Some(mention_uri) = mention_uri.log_err() else {
 753                        continue;
 754                    };
 755                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
 756                        log::error!("failed to parse MIME type for image: {mime_type:?}");
 757                        continue;
 758                    };
 759                    let start = text.len();
 760                    write!(&mut text, "{}", mention_uri.as_link()).ok();
 761                    let end = text.len();
 762                    mentions.push((
 763                        start..end,
 764                        mention_uri,
 765                        Mention::Image(MentionImage {
 766                            data: data.into(),
 767                            format,
 768                        }),
 769                    ));
 770                }
 771                acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
 772            }
 773        }
 774
 775        let snapshot = self.editor.update(cx, |editor, cx| {
 776            editor.set_text(text, window, cx);
 777            editor.buffer().read(cx).snapshot(cx)
 778        });
 779
 780        for (range, mention_uri, mention) in mentions {
 781            let anchor = snapshot.anchor_before(MultiBufferOffset(range.start));
 782            let Some((crease_id, tx)) = insert_crease_for_mention(
 783                anchor.excerpt_id,
 784                anchor.text_anchor,
 785                range.end - range.start,
 786                mention_uri.name().into(),
 787                mention_uri.icon_path(cx),
 788                None,
 789                self.editor.clone(),
 790                window,
 791                cx,
 792            ) else {
 793                continue;
 794            };
 795            drop(tx);
 796
 797            self.mention_set.update(cx, |mention_set, _cx| {
 798                mention_set.insert_mention(
 799                    crease_id,
 800                    mention_uri.clone(),
 801                    Task::ready(Ok(mention)).shared(),
 802                )
 803            });
 804        }
 805        cx.notify();
 806    }
 807
 808    pub fn text(&self, cx: &App) -> String {
 809        self.editor.read(cx).text(cx)
 810    }
 811
 812    pub fn set_placeholder_text(
 813        &mut self,
 814        placeholder: &str,
 815        window: &mut Window,
 816        cx: &mut Context<Self>,
 817    ) {
 818        self.editor.update(cx, |editor, cx| {
 819            editor.set_placeholder_text(placeholder, window, cx);
 820        });
 821    }
 822
 823    #[cfg(test)]
 824    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
 825        self.editor.update(cx, |editor, cx| {
 826            editor.set_text(text, window, cx);
 827        });
 828    }
 829}
 830
 831impl Focusable for MessageEditor {
 832    fn focus_handle(&self, cx: &App) -> FocusHandle {
 833        self.editor.focus_handle(cx)
 834    }
 835}
 836
 837impl Render for MessageEditor {
 838    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 839        div()
 840            .key_context("MessageEditor")
 841            .on_action(cx.listener(Self::chat))
 842            .on_action(cx.listener(Self::chat_with_follow))
 843            .on_action(cx.listener(Self::cancel))
 844            .capture_action(cx.listener(Self::paste))
 845            .flex_1()
 846            .child({
 847                let settings = ThemeSettings::get_global(cx);
 848
 849                let text_style = TextStyle {
 850                    color: cx.theme().colors().text,
 851                    font_family: settings.buffer_font.family.clone(),
 852                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
 853                    font_features: settings.buffer_font.features.clone(),
 854                    font_size: settings.agent_buffer_font_size(cx).into(),
 855                    line_height: relative(settings.buffer_line_height.value()),
 856                    ..Default::default()
 857                };
 858
 859                EditorElement::new(
 860                    &self.editor,
 861                    EditorStyle {
 862                        background: cx.theme().colors().editor_background,
 863                        local_player: cx.theme().players().local(),
 864                        text: text_style,
 865                        syntax: cx.theme().syntax().clone(),
 866                        inlay_hints_style: editor::make_inlay_hints_style(cx),
 867                        ..Default::default()
 868                    },
 869                )
 870            })
 871    }
 872}
 873
 874pub struct MessageEditorAddon {}
 875
 876impl MessageEditorAddon {
 877    pub fn new() -> Self {
 878        Self {}
 879    }
 880}
 881
 882impl Addon for MessageEditorAddon {
 883    fn to_any(&self) -> &dyn std::any::Any {
 884        self
 885    }
 886
 887    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
 888        Some(self)
 889    }
 890
 891    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
 892        let settings = agent_settings::AgentSettings::get_global(cx);
 893        if settings.use_modifier_to_send {
 894            key_context.add("use_modifier_to_send");
 895        }
 896    }
 897}
 898
 899#[cfg(test)]
 900mod tests {
 901    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
 902
 903    use acp_thread::MentionUri;
 904    use agent::{HistoryStore, outline};
 905    use agent_client_protocol as acp;
 906    use assistant_text_thread::TextThreadStore;
 907    use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset};
 908    use fs::FakeFs;
 909    use futures::StreamExt as _;
 910    use gpui::{
 911        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
 912    };
 913    use language_model::LanguageModelRegistry;
 914    use lsp::{CompletionContext, CompletionTriggerKind};
 915    use project::{CompletionIntent, Project, ProjectPath};
 916    use serde_json::json;
 917    use text::Point;
 918    use ui::{App, Context, IntoElement, Render, SharedString, Window};
 919    use util::{path, paths::PathStyle, rel_path::rel_path};
 920    use workspace::{AppState, Item, Workspace};
 921
 922    use crate::acp::{
 923        message_editor::{Mention, MessageEditor},
 924        thread_view::tests::init_test,
 925    };
 926
 927    #[gpui::test]
 928    async fn test_at_mention_removal(cx: &mut TestAppContext) {
 929        init_test(cx);
 930
 931        let fs = FakeFs::new(cx.executor());
 932        fs.insert_tree("/project", json!({"file": ""})).await;
 933        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
 934
 935        let (workspace, cx) =
 936            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 937
 938        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
 939        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
 940
 941        let message_editor = cx.update(|window, cx| {
 942            cx.new(|cx| {
 943                MessageEditor::new(
 944                    workspace.downgrade(),
 945                    project.clone(),
 946                    history_store.clone(),
 947                    None,
 948                    Default::default(),
 949                    Default::default(),
 950                    "Test Agent".into(),
 951                    "Test",
 952                    EditorMode::AutoHeight {
 953                        min_lines: 1,
 954                        max_lines: None,
 955                    },
 956                    window,
 957                    cx,
 958                )
 959            })
 960        });
 961        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
 962
 963        cx.run_until_parked();
 964
 965        let excerpt_id = editor.update(cx, |editor, cx| {
 966            editor
 967                .buffer()
 968                .read(cx)
 969                .excerpt_ids()
 970                .into_iter()
 971                .next()
 972                .unwrap()
 973        });
 974        let completions = editor.update_in(cx, |editor, window, cx| {
 975            editor.set_text("Hello @file ", window, cx);
 976            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
 977            let completion_provider = editor.completion_provider().unwrap();
 978            completion_provider.completions(
 979                excerpt_id,
 980                &buffer,
 981                text::Anchor::MAX,
 982                CompletionContext {
 983                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
 984                    trigger_character: Some("@".into()),
 985                },
 986                window,
 987                cx,
 988            )
 989        });
 990        let [_, completion]: [_; 2] = completions
 991            .await
 992            .unwrap()
 993            .into_iter()
 994            .flat_map(|response| response.completions)
 995            .collect::<Vec<_>>()
 996            .try_into()
 997            .unwrap();
 998
 999        editor.update_in(cx, |editor, window, cx| {
1000            let snapshot = editor.buffer().read(cx).snapshot(cx);
1001            let range = snapshot
1002                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1003                .unwrap();
1004            editor.edit([(range, completion.new_text)], cx);
1005            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1006        });
1007
1008        cx.run_until_parked();
1009
1010        // Backspace over the inserted crease (and the following space).
1011        editor.update_in(cx, |editor, window, cx| {
1012            editor.backspace(&Default::default(), window, cx);
1013            editor.backspace(&Default::default(), window, cx);
1014        });
1015
1016        let (content, _) = message_editor
1017            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1018            .await
1019            .unwrap();
1020
1021        // We don't send a resource link for the deleted crease.
1022        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1023    }
1024
1025    #[gpui::test]
1026    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1027        init_test(cx);
1028        let fs = FakeFs::new(cx.executor());
1029        fs.insert_tree(
1030            "/test",
1031            json!({
1032                ".zed": {
1033                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1034                },
1035                "src": {
1036                    "main.rs": "fn main() {}",
1037                },
1038            }),
1039        )
1040        .await;
1041
1042        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1043        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1044        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1045        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1046        // Start with no available commands - simulating Claude which doesn't support slash commands
1047        let available_commands = Rc::new(RefCell::new(vec![]));
1048
1049        let (workspace, cx) =
1050            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1051        let workspace_handle = workspace.downgrade();
1052        let message_editor = workspace.update_in(cx, |_, window, cx| {
1053            cx.new(|cx| {
1054                MessageEditor::new(
1055                    workspace_handle.clone(),
1056                    project.clone(),
1057                    history_store.clone(),
1058                    None,
1059                    prompt_capabilities.clone(),
1060                    available_commands.clone(),
1061                    "Claude Code".into(),
1062                    "Test",
1063                    EditorMode::AutoHeight {
1064                        min_lines: 1,
1065                        max_lines: None,
1066                    },
1067                    window,
1068                    cx,
1069                )
1070            })
1071        });
1072        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1073
1074        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1075        editor.update_in(cx, |editor, window, cx| {
1076            editor.set_text("/file test.txt", window, cx);
1077        });
1078
1079        let contents_result = message_editor
1080            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1081            .await;
1082
1083        // Should fail because available_commands is empty (no commands supported)
1084        assert!(contents_result.is_err());
1085        let error_message = contents_result.unwrap_err().to_string();
1086        assert!(error_message.contains("not supported by Claude Code"));
1087        assert!(error_message.contains("Available commands: none"));
1088
1089        // Now simulate Claude providing its list of available commands (which doesn't include file)
1090        available_commands.replace(vec![acp::AvailableCommand {
1091            name: "help".to_string(),
1092            description: "Get help".to_string(),
1093            input: None,
1094            meta: None,
1095        }]);
1096
1097        // Test that unsupported slash commands trigger an error when we have a list of available commands
1098        editor.update_in(cx, |editor, window, cx| {
1099            editor.set_text("/file test.txt", window, cx);
1100        });
1101
1102        let contents_result = message_editor
1103            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1104            .await;
1105
1106        assert!(contents_result.is_err());
1107        let error_message = contents_result.unwrap_err().to_string();
1108        assert!(error_message.contains("not supported by Claude Code"));
1109        assert!(error_message.contains("/file"));
1110        assert!(error_message.contains("Available commands: /help"));
1111
1112        // Test that supported commands work fine
1113        editor.update_in(cx, |editor, window, cx| {
1114            editor.set_text("/help", window, cx);
1115        });
1116
1117        let contents_result = message_editor
1118            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1119            .await;
1120
1121        // Should succeed because /help is in available_commands
1122        assert!(contents_result.is_ok());
1123
1124        // Test that regular text works fine
1125        editor.update_in(cx, |editor, window, cx| {
1126            editor.set_text("Hello Claude!", window, cx);
1127        });
1128
1129        let (content, _) = message_editor
1130            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1131            .await
1132            .unwrap();
1133
1134        assert_eq!(content.len(), 1);
1135        if let acp::ContentBlock::Text(text) = &content[0] {
1136            assert_eq!(text.text, "Hello Claude!");
1137        } else {
1138            panic!("Expected ContentBlock::Text");
1139        }
1140
1141        // Test that @ mentions still work
1142        editor.update_in(cx, |editor, window, cx| {
1143            editor.set_text("Check this @", window, cx);
1144        });
1145
1146        // The @ mention functionality should not be affected
1147        let (content, _) = message_editor
1148            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1149            .await
1150            .unwrap();
1151
1152        assert_eq!(content.len(), 1);
1153        if let acp::ContentBlock::Text(text) = &content[0] {
1154            assert_eq!(text.text, "Check this @");
1155        } else {
1156            panic!("Expected ContentBlock::Text");
1157        }
1158    }
1159
1160    struct MessageEditorItem(Entity<MessageEditor>);
1161
1162    impl Item for MessageEditorItem {
1163        type Event = ();
1164
1165        fn include_in_nav_history() -> bool {
1166            false
1167        }
1168
1169        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1170            "Test".into()
1171        }
1172    }
1173
1174    impl EventEmitter<()> for MessageEditorItem {}
1175
1176    impl Focusable for MessageEditorItem {
1177        fn focus_handle(&self, cx: &App) -> FocusHandle {
1178            self.0.read(cx).focus_handle(cx)
1179        }
1180    }
1181
1182    impl Render for MessageEditorItem {
1183        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1184            self.0.clone().into_any_element()
1185        }
1186    }
1187
1188    #[gpui::test]
1189    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1190        init_test(cx);
1191
1192        let app_state = cx.update(AppState::test);
1193
1194        cx.update(|cx| {
1195            editor::init(cx);
1196            workspace::init(app_state.clone(), cx);
1197        });
1198
1199        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1200        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1201        let workspace = window.root(cx).unwrap();
1202
1203        let mut cx = VisualTestContext::from_window(*window, cx);
1204
1205        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1206        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1207        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1208        let available_commands = Rc::new(RefCell::new(vec![
1209            acp::AvailableCommand {
1210                name: "quick-math".to_string(),
1211                description: "2 + 2 = 4 - 1 = 3".to_string(),
1212                input: None,
1213                meta: None,
1214            },
1215            acp::AvailableCommand {
1216                name: "say-hello".to_string(),
1217                description: "Say hello to whoever you want".to_string(),
1218                input: Some(acp::AvailableCommandInput::Unstructured {
1219                    hint: "<name>".to_string(),
1220                }),
1221                meta: None,
1222            },
1223        ]));
1224
1225        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1226            let workspace_handle = cx.weak_entity();
1227            let message_editor = cx.new(|cx| {
1228                MessageEditor::new(
1229                    workspace_handle,
1230                    project.clone(),
1231                    history_store.clone(),
1232                    None,
1233                    prompt_capabilities.clone(),
1234                    available_commands.clone(),
1235                    "Test Agent".into(),
1236                    "Test",
1237                    EditorMode::AutoHeight {
1238                        max_lines: None,
1239                        min_lines: 1,
1240                    },
1241                    window,
1242                    cx,
1243                )
1244            });
1245            workspace.active_pane().update(cx, |pane, cx| {
1246                pane.add_item(
1247                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1248                    true,
1249                    true,
1250                    None,
1251                    window,
1252                    cx,
1253                );
1254            });
1255            message_editor.read(cx).focus_handle(cx).focus(window);
1256            message_editor.read(cx).editor().clone()
1257        });
1258
1259        cx.simulate_input("/");
1260
1261        editor.update_in(&mut cx, |editor, window, cx| {
1262            assert_eq!(editor.text(cx), "/");
1263            assert!(editor.has_visible_completions_menu());
1264
1265            assert_eq!(
1266                current_completion_labels_with_documentation(editor),
1267                &[
1268                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1269                    ("say-hello".into(), "Say hello to whoever you want".into())
1270                ]
1271            );
1272            editor.set_text("", window, cx);
1273        });
1274
1275        cx.simulate_input("/qui");
1276
1277        editor.update_in(&mut cx, |editor, window, cx| {
1278            assert_eq!(editor.text(cx), "/qui");
1279            assert!(editor.has_visible_completions_menu());
1280
1281            assert_eq!(
1282                current_completion_labels_with_documentation(editor),
1283                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1284            );
1285            editor.set_text("", window, cx);
1286        });
1287
1288        editor.update_in(&mut cx, |editor, window, cx| {
1289            assert!(editor.has_visible_completions_menu());
1290            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1291        });
1292
1293        cx.run_until_parked();
1294
1295        editor.update_in(&mut cx, |editor, window, cx| {
1296            assert_eq!(editor.display_text(cx), "/quick-math ");
1297            assert!(!editor.has_visible_completions_menu());
1298            editor.set_text("", window, cx);
1299        });
1300
1301        cx.simulate_input("/say");
1302
1303        editor.update_in(&mut cx, |editor, _window, cx| {
1304            assert_eq!(editor.display_text(cx), "/say");
1305            assert!(editor.has_visible_completions_menu());
1306
1307            assert_eq!(
1308                current_completion_labels_with_documentation(editor),
1309                &[("say-hello".into(), "Say hello to whoever you want".into())]
1310            );
1311        });
1312
1313        editor.update_in(&mut cx, |editor, window, cx| {
1314            assert!(editor.has_visible_completions_menu());
1315            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1316        });
1317
1318        cx.run_until_parked();
1319
1320        editor.update_in(&mut cx, |editor, _window, cx| {
1321            assert_eq!(editor.text(cx), "/say-hello ");
1322            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1323            assert!(!editor.has_visible_completions_menu());
1324        });
1325
1326        cx.simulate_input("GPT5");
1327
1328        cx.run_until_parked();
1329
1330        editor.update_in(&mut cx, |editor, window, cx| {
1331            assert_eq!(editor.text(cx), "/say-hello GPT5");
1332            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
1333            assert!(!editor.has_visible_completions_menu());
1334
1335            // Delete argument
1336            for _ in 0..5 {
1337                editor.backspace(&editor::actions::Backspace, window, cx);
1338            }
1339        });
1340
1341        cx.run_until_parked();
1342
1343        editor.update_in(&mut cx, |editor, window, cx| {
1344            assert_eq!(editor.text(cx), "/say-hello");
1345            // Hint is visible because argument was deleted
1346            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1347
1348            // Delete last command letter
1349            editor.backspace(&editor::actions::Backspace, window, cx);
1350        });
1351
1352        cx.run_until_parked();
1353
1354        editor.update_in(&mut cx, |editor, _window, cx| {
1355            // Hint goes away once command no longer matches an available one
1356            assert_eq!(editor.text(cx), "/say-hell");
1357            assert_eq!(editor.display_text(cx), "/say-hell");
1358            assert!(!editor.has_visible_completions_menu());
1359        });
1360    }
1361
1362    #[gpui::test]
1363    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
1364        init_test(cx);
1365
1366        let app_state = cx.update(AppState::test);
1367
1368        cx.update(|cx| {
1369            editor::init(cx);
1370            workspace::init(app_state.clone(), cx);
1371        });
1372
1373        app_state
1374            .fs
1375            .as_fake()
1376            .insert_tree(
1377                path!("/dir"),
1378                json!({
1379                    "editor": "",
1380                    "a": {
1381                        "one.txt": "1",
1382                        "two.txt": "2",
1383                        "three.txt": "3",
1384                        "four.txt": "4"
1385                    },
1386                    "b": {
1387                        "five.txt": "5",
1388                        "six.txt": "6",
1389                        "seven.txt": "7",
1390                        "eight.txt": "8",
1391                    },
1392                    "x.png": "",
1393                }),
1394            )
1395            .await;
1396
1397        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1398        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1399        let workspace = window.root(cx).unwrap();
1400
1401        let worktree = project.update(cx, |project, cx| {
1402            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1403            assert_eq!(worktrees.len(), 1);
1404            worktrees.pop().unwrap()
1405        });
1406        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1407
1408        let mut cx = VisualTestContext::from_window(*window, cx);
1409
1410        let paths = vec![
1411            rel_path("a/one.txt"),
1412            rel_path("a/two.txt"),
1413            rel_path("a/three.txt"),
1414            rel_path("a/four.txt"),
1415            rel_path("b/five.txt"),
1416            rel_path("b/six.txt"),
1417            rel_path("b/seven.txt"),
1418            rel_path("b/eight.txt"),
1419        ];
1420
1421        let slash = PathStyle::local().separator();
1422
1423        let mut opened_editors = Vec::new();
1424        for path in paths {
1425            let buffer = workspace
1426                .update_in(&mut cx, |workspace, window, cx| {
1427                    workspace.open_path(
1428                        ProjectPath {
1429                            worktree_id,
1430                            path: path.into(),
1431                        },
1432                        None,
1433                        false,
1434                        window,
1435                        cx,
1436                    )
1437                })
1438                .await
1439                .unwrap();
1440            opened_editors.push(buffer);
1441        }
1442
1443        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1444        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1445        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1446
1447        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1448            let workspace_handle = cx.weak_entity();
1449            let message_editor = cx.new(|cx| {
1450                MessageEditor::new(
1451                    workspace_handle,
1452                    project.clone(),
1453                    history_store.clone(),
1454                    None,
1455                    prompt_capabilities.clone(),
1456                    Default::default(),
1457                    "Test Agent".into(),
1458                    "Test",
1459                    EditorMode::AutoHeight {
1460                        max_lines: None,
1461                        min_lines: 1,
1462                    },
1463                    window,
1464                    cx,
1465                )
1466            });
1467            workspace.active_pane().update(cx, |pane, cx| {
1468                pane.add_item(
1469                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1470                    true,
1471                    true,
1472                    None,
1473                    window,
1474                    cx,
1475                );
1476            });
1477            message_editor.read(cx).focus_handle(cx).focus(window);
1478            let editor = message_editor.read(cx).editor().clone();
1479            (message_editor, editor)
1480        });
1481
1482        cx.simulate_input("Lorem @");
1483
1484        editor.update_in(&mut cx, |editor, window, cx| {
1485            assert_eq!(editor.text(cx), "Lorem @");
1486            assert!(editor.has_visible_completions_menu());
1487
1488            assert_eq!(
1489                current_completion_labels(editor),
1490                &[
1491                    format!("eight.txt b{slash}"),
1492                    format!("seven.txt b{slash}"),
1493                    format!("six.txt b{slash}"),
1494                    format!("five.txt b{slash}"),
1495                    "Files & Directories".into(),
1496                    "Symbols".into()
1497                ]
1498            );
1499            editor.set_text("", window, cx);
1500        });
1501
1502        prompt_capabilities.replace(acp::PromptCapabilities {
1503            image: true,
1504            audio: true,
1505            embedded_context: true,
1506            meta: None,
1507        });
1508
1509        cx.simulate_input("Lorem ");
1510
1511        editor.update(&mut cx, |editor, cx| {
1512            assert_eq!(editor.text(cx), "Lorem ");
1513            assert!(!editor.has_visible_completions_menu());
1514        });
1515
1516        cx.simulate_input("@");
1517
1518        editor.update(&mut cx, |editor, cx| {
1519            assert_eq!(editor.text(cx), "Lorem @");
1520            assert!(editor.has_visible_completions_menu());
1521            assert_eq!(
1522                current_completion_labels(editor),
1523                &[
1524                    format!("eight.txt b{slash}"),
1525                    format!("seven.txt b{slash}"),
1526                    format!("six.txt b{slash}"),
1527                    format!("five.txt b{slash}"),
1528                    "Files & Directories".into(),
1529                    "Symbols".into(),
1530                    "Threads".into(),
1531                    "Fetch".into()
1532                ]
1533            );
1534        });
1535
1536        // Select and confirm "File"
1537        editor.update_in(&mut cx, |editor, window, cx| {
1538            assert!(editor.has_visible_completions_menu());
1539            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1540            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1541            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1542            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1543            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1544        });
1545
1546        cx.run_until_parked();
1547
1548        editor.update(&mut cx, |editor, cx| {
1549            assert_eq!(editor.text(cx), "Lorem @file ");
1550            assert!(editor.has_visible_completions_menu());
1551        });
1552
1553        cx.simulate_input("one");
1554
1555        editor.update(&mut cx, |editor, cx| {
1556            assert_eq!(editor.text(cx), "Lorem @file one");
1557            assert!(editor.has_visible_completions_menu());
1558            assert_eq!(
1559                current_completion_labels(editor),
1560                vec![format!("one.txt a{slash}")]
1561            );
1562        });
1563
1564        editor.update_in(&mut cx, |editor, window, cx| {
1565            assert!(editor.has_visible_completions_menu());
1566            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1567        });
1568
1569        let url_one = MentionUri::File {
1570            abs_path: path!("/dir/a/one.txt").into(),
1571        }
1572        .to_uri()
1573        .to_string();
1574        editor.update(&mut cx, |editor, cx| {
1575            let text = editor.text(cx);
1576            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
1577            assert!(!editor.has_visible_completions_menu());
1578            assert_eq!(fold_ranges(editor, cx).len(), 1);
1579        });
1580
1581        let contents = message_editor
1582            .update(&mut cx, |message_editor, cx| {
1583                message_editor
1584                    .mention_set()
1585                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1586            })
1587            .await
1588            .unwrap()
1589            .into_values()
1590            .collect::<Vec<_>>();
1591
1592        {
1593            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
1594                panic!("Unexpected mentions");
1595            };
1596            pretty_assertions::assert_eq!(content, "1");
1597            pretty_assertions::assert_eq!(
1598                uri,
1599                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
1600            );
1601        }
1602
1603        cx.simulate_input(" ");
1604
1605        editor.update(&mut cx, |editor, cx| {
1606            let text = editor.text(cx);
1607            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
1608            assert!(!editor.has_visible_completions_menu());
1609            assert_eq!(fold_ranges(editor, cx).len(), 1);
1610        });
1611
1612        cx.simulate_input("Ipsum ");
1613
1614        editor.update(&mut cx, |editor, cx| {
1615            let text = editor.text(cx);
1616            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
1617            assert!(!editor.has_visible_completions_menu());
1618            assert_eq!(fold_ranges(editor, cx).len(), 1);
1619        });
1620
1621        cx.simulate_input("@file ");
1622
1623        editor.update(&mut cx, |editor, cx| {
1624            let text = editor.text(cx);
1625            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
1626            assert!(editor.has_visible_completions_menu());
1627            assert_eq!(fold_ranges(editor, cx).len(), 1);
1628        });
1629
1630        editor.update_in(&mut cx, |editor, window, cx| {
1631            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1632        });
1633
1634        cx.run_until_parked();
1635
1636        let contents = message_editor
1637            .update(&mut cx, |message_editor, cx| {
1638                message_editor
1639                    .mention_set()
1640                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1641            })
1642            .await
1643            .unwrap()
1644            .into_values()
1645            .collect::<Vec<_>>();
1646
1647        let url_eight = MentionUri::File {
1648            abs_path: path!("/dir/b/eight.txt").into(),
1649        }
1650        .to_uri()
1651        .to_string();
1652
1653        {
1654            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
1655                panic!("Unexpected mentions");
1656            };
1657            pretty_assertions::assert_eq!(content, "8");
1658            pretty_assertions::assert_eq!(
1659                uri,
1660                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
1661            );
1662        }
1663
1664        editor.update(&mut cx, |editor, cx| {
1665            assert_eq!(
1666                editor.text(cx),
1667                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
1668            );
1669            assert!(!editor.has_visible_completions_menu());
1670            assert_eq!(fold_ranges(editor, cx).len(), 2);
1671        });
1672
1673        let plain_text_language = Arc::new(language::Language::new(
1674            language::LanguageConfig {
1675                name: "Plain Text".into(),
1676                matcher: language::LanguageMatcher {
1677                    path_suffixes: vec!["txt".to_string()],
1678                    ..Default::default()
1679                },
1680                ..Default::default()
1681            },
1682            None,
1683        ));
1684
1685        // Register the language and fake LSP
1686        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1687        language_registry.add(plain_text_language);
1688
1689        let mut fake_language_servers = language_registry.register_fake_lsp(
1690            "Plain Text",
1691            language::FakeLspAdapter {
1692                capabilities: lsp::ServerCapabilities {
1693                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1694                    ..Default::default()
1695                },
1696                ..Default::default()
1697            },
1698        );
1699
1700        // Open the buffer to trigger LSP initialization
1701        let buffer = project
1702            .update(&mut cx, |project, cx| {
1703                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1704            })
1705            .await
1706            .unwrap();
1707
1708        // Register the buffer with language servers
1709        let _handle = project.update(&mut cx, |project, cx| {
1710            project.register_buffer_with_language_servers(&buffer, cx)
1711        });
1712
1713        cx.run_until_parked();
1714
1715        let fake_language_server = fake_language_servers.next().await.unwrap();
1716        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1717            move |_, _| async move {
1718                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1719                    #[allow(deprecated)]
1720                    lsp::SymbolInformation {
1721                        name: "MySymbol".into(),
1722                        location: lsp::Location {
1723                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1724                            range: lsp::Range::new(
1725                                lsp::Position::new(0, 0),
1726                                lsp::Position::new(0, 1),
1727                            ),
1728                        },
1729                        kind: lsp::SymbolKind::CONSTANT,
1730                        tags: None,
1731                        container_name: None,
1732                        deprecated: None,
1733                    },
1734                ])))
1735            },
1736        );
1737
1738        cx.simulate_input("@symbol ");
1739
1740        editor.update(&mut cx, |editor, cx| {
1741            assert_eq!(
1742                editor.text(cx),
1743                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
1744            );
1745            assert!(editor.has_visible_completions_menu());
1746            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
1747        });
1748
1749        editor.update_in(&mut cx, |editor, window, cx| {
1750            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1751        });
1752
1753        let symbol = MentionUri::Symbol {
1754            abs_path: path!("/dir/a/one.txt").into(),
1755            name: "MySymbol".into(),
1756            line_range: 0..=0,
1757        };
1758
1759        let contents = message_editor
1760            .update(&mut cx, |message_editor, cx| {
1761                message_editor
1762                    .mention_set()
1763                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1764            })
1765            .await
1766            .unwrap()
1767            .into_values()
1768            .collect::<Vec<_>>();
1769
1770        {
1771            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
1772                panic!("Unexpected mentions");
1773            };
1774            pretty_assertions::assert_eq!(content, "1");
1775            pretty_assertions::assert_eq!(uri, &symbol);
1776        }
1777
1778        cx.run_until_parked();
1779
1780        editor.read_with(&cx, |editor, cx| {
1781            assert_eq!(
1782                editor.text(cx),
1783                format!(
1784                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1785                    symbol.to_uri(),
1786                )
1787            );
1788        });
1789
1790        // Try to mention an "image" file that will fail to load
1791        cx.simulate_input("@file x.png");
1792
1793        editor.update(&mut cx, |editor, cx| {
1794            assert_eq!(
1795                editor.text(cx),
1796                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
1797            );
1798            assert!(editor.has_visible_completions_menu());
1799            assert_eq!(current_completion_labels(editor), &["x.png "]);
1800        });
1801
1802        editor.update_in(&mut cx, |editor, window, cx| {
1803            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1804        });
1805
1806        // Getting the message contents fails
1807        message_editor
1808            .update(&mut cx, |message_editor, cx| {
1809                message_editor
1810                    .mention_set()
1811                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1812            })
1813            .await
1814            .expect_err("Should fail to load x.png");
1815
1816        cx.run_until_parked();
1817
1818        // Mention was removed
1819        editor.read_with(&cx, |editor, cx| {
1820            assert_eq!(
1821                editor.text(cx),
1822                format!(
1823                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1824                    symbol.to_uri()
1825                )
1826            );
1827        });
1828
1829        // Once more
1830        cx.simulate_input("@file x.png");
1831
1832        editor.update(&mut cx, |editor, cx| {
1833                    assert_eq!(
1834                        editor.text(cx),
1835                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
1836                    );
1837                    assert!(editor.has_visible_completions_menu());
1838                    assert_eq!(current_completion_labels(editor), &["x.png "]);
1839                });
1840
1841        editor.update_in(&mut cx, |editor, window, cx| {
1842            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1843        });
1844
1845        // This time don't immediately get the contents, just let the confirmed completion settle
1846        cx.run_until_parked();
1847
1848        // Mention was removed
1849        editor.read_with(&cx, |editor, cx| {
1850            assert_eq!(
1851                editor.text(cx),
1852                format!(
1853                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1854                    symbol.to_uri()
1855                )
1856            );
1857        });
1858
1859        // Now getting the contents succeeds, because the invalid mention was removed
1860        let contents = message_editor
1861            .update(&mut cx, |message_editor, cx| {
1862                message_editor
1863                    .mention_set()
1864                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1865            })
1866            .await
1867            .unwrap();
1868        assert_eq!(contents.len(), 3);
1869    }
1870
1871    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1872        let snapshot = editor.buffer().read(cx).snapshot(cx);
1873        editor.display_map.update(cx, |display_map, cx| {
1874            display_map
1875                .snapshot(cx)
1876                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
1877                .map(|fold| fold.range.to_point(&snapshot))
1878                .collect()
1879        })
1880    }
1881
1882    fn current_completion_labels(editor: &Editor) -> Vec<String> {
1883        let completions = editor.current_completions().expect("Missing completions");
1884        completions
1885            .into_iter()
1886            .map(|completion| completion.label.text)
1887            .collect::<Vec<_>>()
1888    }
1889
1890    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
1891        let completions = editor.current_completions().expect("Missing completions");
1892        completions
1893            .into_iter()
1894            .map(|completion| {
1895                (
1896                    completion.label.text,
1897                    completion
1898                        .documentation
1899                        .map(|d| d.text().to_string())
1900                        .unwrap_or_default(),
1901                )
1902            })
1903            .collect::<Vec<_>>()
1904    }
1905
1906    #[gpui::test]
1907    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
1908        init_test(cx);
1909
1910        let fs = FakeFs::new(cx.executor());
1911
1912        // Create a large file that exceeds AUTO_OUTLINE_SIZE
1913        // Using plain text without a configured language, so no outline is available
1914        const LINE: &str = "This is a line of text in the file\n";
1915        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
1916        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
1917
1918        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
1919        let small_content = "fn small_function() { /* small */ }\n";
1920        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
1921
1922        fs.insert_tree(
1923            "/project",
1924            json!({
1925                "large_file.txt": large_content.clone(),
1926                "small_file.txt": small_content,
1927            }),
1928        )
1929        .await;
1930
1931        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1932
1933        let (workspace, cx) =
1934            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1935
1936        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1937        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1938
1939        let message_editor = cx.update(|window, cx| {
1940            cx.new(|cx| {
1941                let editor = MessageEditor::new(
1942                    workspace.downgrade(),
1943                    project.clone(),
1944                    history_store.clone(),
1945                    None,
1946                    Default::default(),
1947                    Default::default(),
1948                    "Test Agent".into(),
1949                    "Test",
1950                    EditorMode::AutoHeight {
1951                        min_lines: 1,
1952                        max_lines: None,
1953                    },
1954                    window,
1955                    cx,
1956                );
1957                // Enable embedded context so files are actually included
1958                editor.prompt_capabilities.replace(acp::PromptCapabilities {
1959                    embedded_context: true,
1960                    meta: None,
1961                    ..Default::default()
1962                });
1963                editor
1964            })
1965        });
1966
1967        // Test large file mention
1968        // Get the absolute path using the project's worktree
1969        let large_file_abs_path = project.read_with(cx, |project, cx| {
1970            let worktree = project.worktrees(cx).next().unwrap();
1971            let worktree_root = worktree.read(cx).abs_path();
1972            worktree_root.join("large_file.txt")
1973        });
1974        let large_file_task = message_editor.update(cx, |editor, cx| {
1975            editor.mention_set().update(cx, |set, cx| {
1976                set.confirm_mention_for_file(large_file_abs_path, true, cx)
1977            })
1978        });
1979
1980        let large_file_mention = large_file_task.await.unwrap();
1981        match large_file_mention {
1982            Mention::Text { content, .. } => {
1983                // Should contain some of the content but not all of it
1984                assert!(
1985                    content.contains(LINE),
1986                    "Should contain some of the file content"
1987                );
1988                assert!(
1989                    !content.contains(&LINE.repeat(100)),
1990                    "Should not contain the full file"
1991                );
1992                // Should be much smaller than original
1993                assert!(
1994                    content.len() < large_content.len() / 10,
1995                    "Should be significantly truncated"
1996                );
1997            }
1998            _ => panic!("Expected Text mention for large file"),
1999        }
2000
2001        // Test small file mention
2002        // Get the absolute path using the project's worktree
2003        let small_file_abs_path = project.read_with(cx, |project, cx| {
2004            let worktree = project.worktrees(cx).next().unwrap();
2005            let worktree_root = worktree.read(cx).abs_path();
2006            worktree_root.join("small_file.txt")
2007        });
2008        let small_file_task = message_editor.update(cx, |editor, cx| {
2009            editor.mention_set().update(cx, |set, cx| {
2010                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2011            })
2012        });
2013
2014        let small_file_mention = small_file_task.await.unwrap();
2015        match small_file_mention {
2016            Mention::Text { content, .. } => {
2017                // Should contain the full actual content
2018                assert_eq!(content, small_content);
2019            }
2020            _ => panic!("Expected Text mention for small file"),
2021        }
2022    }
2023
2024    #[gpui::test]
2025    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2026        init_test(cx);
2027        cx.update(LanguageModelRegistry::test);
2028
2029        let fs = FakeFs::new(cx.executor());
2030        fs.insert_tree("/project", json!({"file": ""})).await;
2031        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2032
2033        let (workspace, cx) =
2034            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2035
2036        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2037        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2038
2039        // Create a thread metadata to insert as summary
2040        let thread_metadata = agent::DbThreadMetadata {
2041            id: acp::SessionId("thread-123".into()),
2042            title: "Previous Conversation".into(),
2043            updated_at: chrono::Utc::now(),
2044        };
2045
2046        let message_editor = cx.update(|window, cx| {
2047            cx.new(|cx| {
2048                let mut editor = MessageEditor::new(
2049                    workspace.downgrade(),
2050                    project.clone(),
2051                    history_store.clone(),
2052                    None,
2053                    Default::default(),
2054                    Default::default(),
2055                    "Test Agent".into(),
2056                    "Test",
2057                    EditorMode::AutoHeight {
2058                        min_lines: 1,
2059                        max_lines: None,
2060                    },
2061                    window,
2062                    cx,
2063                );
2064                editor.insert_thread_summary(thread_metadata.clone(), window, cx);
2065                editor
2066            })
2067        });
2068
2069        // Construct expected values for verification
2070        let expected_uri = MentionUri::Thread {
2071            id: thread_metadata.id.clone(),
2072            name: thread_metadata.title.to_string(),
2073        };
2074        let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri());
2075
2076        message_editor.read_with(cx, |editor, cx| {
2077            let text = editor.text(cx);
2078
2079            assert!(
2080                text.contains(&expected_link),
2081                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2082                expected_link,
2083                text
2084            );
2085
2086            let mentions = editor.mention_set().read(cx).mentions();
2087            assert_eq!(
2088                mentions.len(),
2089                1,
2090                "Expected exactly one mention after inserting thread summary"
2091            );
2092
2093            assert!(
2094                mentions.contains(&expected_uri),
2095                "Expected mentions to contain the thread URI"
2096            );
2097        });
2098    }
2099
2100    #[gpui::test]
2101    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
2102        init_test(cx);
2103
2104        let fs = FakeFs::new(cx.executor());
2105        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
2106            .await;
2107        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2108
2109        let (workspace, cx) =
2110            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2111
2112        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2113        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2114
2115        let message_editor = cx.update(|window, cx| {
2116            cx.new(|cx| {
2117                MessageEditor::new(
2118                    workspace.downgrade(),
2119                    project.clone(),
2120                    history_store.clone(),
2121                    None,
2122                    Default::default(),
2123                    Default::default(),
2124                    "Test Agent".into(),
2125                    "Test",
2126                    EditorMode::AutoHeight {
2127                        min_lines: 1,
2128                        max_lines: None,
2129                    },
2130                    window,
2131                    cx,
2132                )
2133            })
2134        });
2135        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2136
2137        cx.run_until_parked();
2138
2139        editor.update_in(cx, |editor, window, cx| {
2140            editor.set_text("  \u{A0}してhello world  ", window, cx);
2141        });
2142
2143        let (content, _) = message_editor
2144            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2145            .await
2146            .unwrap();
2147
2148        assert_eq!(
2149            content,
2150            vec![acp::ContentBlock::Text(acp::TextContent {
2151                text: "してhello world".into(),
2152                annotations: None,
2153                meta: None
2154            })]
2155        );
2156    }
2157
2158    #[gpui::test]
2159    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
2160        init_test(cx);
2161
2162        let fs = FakeFs::new(cx.executor());
2163
2164        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
2165
2166        fs.insert_tree(
2167            "/project",
2168            json!({
2169                "src": {
2170                    "main.rs": file_content,
2171                }
2172            }),
2173        )
2174        .await;
2175
2176        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2177
2178        let (workspace, cx) =
2179            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2180
2181        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2182        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2183
2184        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
2185            let workspace_handle = cx.weak_entity();
2186            let message_editor = cx.new(|cx| {
2187                MessageEditor::new(
2188                    workspace_handle,
2189                    project.clone(),
2190                    history_store.clone(),
2191                    None,
2192                    Default::default(),
2193                    Default::default(),
2194                    "Test Agent".into(),
2195                    "Test",
2196                    EditorMode::AutoHeight {
2197                        max_lines: None,
2198                        min_lines: 1,
2199                    },
2200                    window,
2201                    cx,
2202                )
2203            });
2204            workspace.active_pane().update(cx, |pane, cx| {
2205                pane.add_item(
2206                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2207                    true,
2208                    true,
2209                    None,
2210                    window,
2211                    cx,
2212                );
2213            });
2214            message_editor.read(cx).focus_handle(cx).focus(window);
2215            let editor = message_editor.read(cx).editor().clone();
2216            (message_editor, editor)
2217        });
2218
2219        cx.simulate_input("What is in @file main");
2220
2221        editor.update_in(cx, |editor, window, cx| {
2222            assert!(editor.has_visible_completions_menu());
2223            assert_eq!(editor.text(cx), "What is in @file main");
2224            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2225        });
2226
2227        let content = message_editor
2228            .update(cx, |editor, cx| editor.contents(false, cx))
2229            .await
2230            .unwrap()
2231            .0;
2232
2233        let main_rs_uri = if cfg!(windows) {
2234            "file:///C:/project/src/main.rs".to_string()
2235        } else {
2236            "file:///project/src/main.rs".to_string()
2237        };
2238
2239        // When embedded context is `false` we should get a resource link
2240        pretty_assertions::assert_eq!(
2241            content,
2242            vec![
2243                acp::ContentBlock::Text(acp::TextContent {
2244                    text: "What is in ".to_string(),
2245                    annotations: None,
2246                    meta: None
2247                }),
2248                acp::ContentBlock::ResourceLink(acp::ResourceLink {
2249                    uri: main_rs_uri.clone(),
2250                    name: "main.rs".to_string(),
2251                    annotations: None,
2252                    meta: None,
2253                    description: None,
2254                    mime_type: None,
2255                    size: None,
2256                    title: None,
2257                })
2258            ]
2259        );
2260
2261        message_editor.update(cx, |editor, _cx| {
2262            editor.prompt_capabilities.replace(acp::PromptCapabilities {
2263                embedded_context: true,
2264                ..Default::default()
2265            })
2266        });
2267
2268        let content = message_editor
2269            .update(cx, |editor, cx| editor.contents(false, cx))
2270            .await
2271            .unwrap()
2272            .0;
2273
2274        // When embedded context is `true` we should get a resource
2275        pretty_assertions::assert_eq!(
2276            content,
2277            vec![
2278                acp::ContentBlock::Text(acp::TextContent {
2279                    text: "What is in ".to_string(),
2280                    annotations: None,
2281                    meta: None
2282                }),
2283                acp::ContentBlock::Resource(acp::EmbeddedResource {
2284                    resource: acp::EmbeddedResourceResource::TextResourceContents(
2285                        acp::TextResourceContents {
2286                            text: file_content.to_string(),
2287                            uri: main_rs_uri,
2288                            mime_type: None,
2289                            meta: None
2290                        }
2291                    ),
2292                    annotations: None,
2293                    meta: None
2294                })
2295            ]
2296        );
2297    }
2298
2299    #[gpui::test]
2300    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
2301        init_test(cx);
2302
2303        let app_state = cx.update(AppState::test);
2304
2305        cx.update(|cx| {
2306            editor::init(cx);
2307            workspace::init(app_state.clone(), cx);
2308        });
2309
2310        app_state
2311            .fs
2312            .as_fake()
2313            .insert_tree(
2314                path!("/dir"),
2315                json!({
2316                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
2317                }),
2318            )
2319            .await;
2320
2321        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2322        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2323        let workspace = window.root(cx).unwrap();
2324
2325        let worktree = project.update(cx, |project, cx| {
2326            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2327            assert_eq!(worktrees.len(), 1);
2328            worktrees.pop().unwrap()
2329        });
2330        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2331
2332        let mut cx = VisualTestContext::from_window(*window, cx);
2333
2334        // Open a regular editor with the created file, and select a portion of
2335        // the text that will be used for the selections that are meant to be
2336        // inserted in the agent panel.
2337        let editor = workspace
2338            .update_in(&mut cx, |workspace, window, cx| {
2339                workspace.open_path(
2340                    ProjectPath {
2341                        worktree_id,
2342                        path: rel_path("test.txt").into(),
2343                    },
2344                    None,
2345                    false,
2346                    window,
2347                    cx,
2348                )
2349            })
2350            .await
2351            .unwrap()
2352            .downcast::<Editor>()
2353            .unwrap();
2354
2355        editor.update_in(&mut cx, |editor, window, cx| {
2356            editor.change_selections(Default::default(), window, cx, |selections| {
2357                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
2358            });
2359        });
2360
2361        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2362        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2363
2364        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
2365        // to ensure we have a fixed viewport, so we can eventually actually
2366        // place the cursor outside of the visible area.
2367        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2368            let workspace_handle = cx.weak_entity();
2369            let message_editor = cx.new(|cx| {
2370                MessageEditor::new(
2371                    workspace_handle,
2372                    project.clone(),
2373                    history_store.clone(),
2374                    None,
2375                    Default::default(),
2376                    Default::default(),
2377                    "Test Agent".into(),
2378                    "Test",
2379                    EditorMode::full(),
2380                    window,
2381                    cx,
2382                )
2383            });
2384            workspace.active_pane().update(cx, |pane, cx| {
2385                pane.add_item(
2386                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2387                    true,
2388                    true,
2389                    None,
2390                    window,
2391                    cx,
2392                );
2393            });
2394
2395            message_editor
2396        });
2397
2398        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2399            message_editor.editor.update(cx, |editor, cx| {
2400                // Update the Agent Panel's Message Editor text to have 100
2401                // lines, ensuring that the cursor is set at line 90 and that we
2402                // then scroll all the way to the top, so the cursor's position
2403                // remains off screen.
2404                let mut lines = String::new();
2405                for _ in 1..=100 {
2406                    lines.push_str(&"Another line in the agent panel's message editor\n");
2407                }
2408                editor.set_text(lines.as_str(), window, cx);
2409                editor.change_selections(Default::default(), window, cx, |selections| {
2410                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
2411                });
2412                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
2413            });
2414        });
2415
2416        cx.run_until_parked();
2417
2418        // Before proceeding, let's assert that the cursor is indeed off screen,
2419        // otherwise the rest of the test doesn't make sense.
2420        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2421            message_editor.editor.update(cx, |editor, cx| {
2422                let snapshot = editor.snapshot(window, cx);
2423                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
2424                let scroll_top = snapshot.scroll_position().y as u32;
2425                let visible_lines = editor.visible_line_count().unwrap() as u32;
2426                let visible_range = scroll_top..(scroll_top + visible_lines);
2427
2428                assert!(!visible_range.contains(&cursor_row));
2429            })
2430        });
2431
2432        // Now let's insert the selection in the Agent Panel's editor and
2433        // confirm that, after the insertion, the cursor is now in the visible
2434        // range.
2435        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2436            message_editor.insert_selections(window, cx);
2437        });
2438
2439        cx.run_until_parked();
2440
2441        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2442            message_editor.editor.update(cx, |editor, cx| {
2443                let snapshot = editor.snapshot(window, cx);
2444                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
2445                let scroll_top = snapshot.scroll_position().y as u32;
2446                let visible_lines = editor.visible_line_count().unwrap() as u32;
2447                let visible_range = scroll_top..(scroll_top + visible_lines);
2448
2449                assert!(visible_range.contains(&cursor_row));
2450            })
2451        });
2452    }
2453}