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