message_editor.rs

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