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