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