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