message_editor.rs

   1use crate::SendImmediately;
   2use crate::ThreadHistory;
   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::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::AgentId;
  31use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
  32use prompt_store::PromptStore;
  33use rope::Point;
  34use settings::Settings;
  35use std::{cell::RefCell, fmt::Write, ops::Range, rc::Rc, sync::Arc};
  36use theme::ThemeSettings;
  37use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};
  38use util::paths::PathStyle;
  39use util::{ResultExt, debug_panic};
  40use workspace::{CollaboratorId, Workspace};
  41use zed_actions::agent::{Chat, PasteRaw};
  42
  43pub struct MessageEditor {
  44    mention_set: Entity<MentionSet>,
  45    editor: Entity<Editor>,
  46    workspace: WeakEntity<Workspace>,
  47    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
  48    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  49    agent_id: AgentId,
  50    thread_store: Option<Entity<ThreadStore>>,
  51    _subscriptions: Vec<Subscription>,
  52    _parse_slash_command_task: Task<()>,
  53}
  54
  55#[derive(Clone, Debug)]
  56pub enum MessageEditorEvent {
  57    Send,
  58    SendImmediately,
  59    Cancel,
  60    Focus,
  61    LostFocus,
  62    InputAttempted(Arc<str>),
  63}
  64
  65impl EventEmitter<MessageEditorEvent> for MessageEditor {}
  66
  67const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
  68
  69impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
  70    fn supports_images(&self, cx: &App) -> bool {
  71        self.read(cx).prompt_capabilities.borrow().image
  72    }
  73
  74    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
  75        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
  76        if self.read(cx).prompt_capabilities.borrow().embedded_context {
  77            if self.read(cx).thread_store.is_some() {
  78                supported.push(PromptContextType::Thread);
  79            }
  80            supported.extend(&[
  81                PromptContextType::Diagnostics,
  82                PromptContextType::Fetch,
  83                PromptContextType::Rules,
  84                PromptContextType::BranchDiff,
  85            ]);
  86        }
  87        supported
  88    }
  89
  90    fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
  91        self.read(cx)
  92            .available_commands
  93            .borrow()
  94            .iter()
  95            .map(|cmd| crate::completion_provider::AvailableCommand {
  96                name: cmd.name.clone().into(),
  97                description: cmd.description.clone().into(),
  98                requires_argument: cmd.input.is_some(),
  99            })
 100            .collect()
 101    }
 102
 103    fn confirm_command(&self, cx: &mut App) {
 104        self.update(cx, |this, cx| this.send(cx));
 105    }
 106}
 107
 108impl MessageEditor {
 109    pub fn new(
 110        workspace: WeakEntity<Workspace>,
 111        project: WeakEntity<Project>,
 112        thread_store: Option<Entity<ThreadStore>>,
 113        history: WeakEntity<ThreadHistory>,
 114        prompt_store: Option<Entity<PromptStore>>,
 115        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
 116        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 117        agent_id: AgentId,
 118        placeholder: &str,
 119        mode: EditorMode,
 120        window: &mut Window,
 121        cx: &mut Context<Self>,
 122    ) -> Self {
 123        let language = Language::new(
 124            language::LanguageConfig {
 125                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 126                ..Default::default()
 127            },
 128            None,
 129        );
 130
 131        let editor = cx.new(|cx| {
 132            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 133            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 134
 135            let mut editor = Editor::new(mode, buffer, None, window, cx);
 136            editor.set_placeholder_text(placeholder, window, cx);
 137            editor.set_show_indent_guides(false, cx);
 138            editor.set_show_completions_on_input(Some(true));
 139            editor.set_soft_wrap();
 140            editor.set_use_modal_editing(true);
 141            editor.set_context_menu_options(ContextMenuOptions {
 142                min_entries_visible: 12,
 143                max_entries_visible: 12,
 144                placement: Some(ContextMenuPlacement::Above),
 145            });
 146            editor.register_addon(MessageEditorAddon::new());
 147
 148            editor.set_custom_context_menu(|editor, _point, window, cx| {
 149                let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
 150
 151                Some(ContextMenu::build(window, cx, |menu, _, _| {
 152                    menu.action("Cut", Box::new(editor::actions::Cut))
 153                        .action_disabled_when(
 154                            !has_selection,
 155                            "Copy",
 156                            Box::new(editor::actions::Copy),
 157                        )
 158                        .action("Paste", Box::new(editor::actions::Paste))
 159                        .action("Paste as Plain Text", Box::new(PasteRaw))
 160                }))
 161            });
 162
 163            editor
 164        });
 165        let mention_set =
 166            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
 167        let completion_provider = Rc::new(PromptCompletionProvider::new(
 168            cx.entity(),
 169            editor.downgrade(),
 170            mention_set.clone(),
 171            history,
 172            prompt_store.clone(),
 173            workspace.clone(),
 174        ));
 175        editor.update(cx, |editor, _cx| {
 176            editor.set_completion_provider(Some(completion_provider.clone()))
 177        });
 178
 179        cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
 180            cx.emit(MessageEditorEvent::Focus)
 181        })
 182        .detach();
 183        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
 184            cx.emit(MessageEditorEvent::LostFocus)
 185        })
 186        .detach();
 187
 188        let mut has_hint = false;
 189        let mut subscriptions = Vec::new();
 190
 191        subscriptions.push(cx.subscribe_in(&editor, window, {
 192            move |this, editor, event, window, cx| {
 193                let input_attempted_text = match event {
 194                    EditorEvent::InputHandled { text, .. } => Some(text),
 195                    EditorEvent::InputIgnored { text } => Some(text),
 196                    _ => None,
 197                };
 198                if let Some(text) = input_attempted_text
 199                    && editor.read(cx).read_only(cx)
 200                    && !text.is_empty()
 201                {
 202                    cx.emit(MessageEditorEvent::InputAttempted(text.clone()));
 203                }
 204
 205                if let EditorEvent::Edited { .. } = event
 206                    && !editor.read(cx).read_only(cx)
 207                {
 208                    editor.update(cx, |editor, cx| {
 209                        let snapshot = editor.snapshot(window, cx);
 210                        this.mention_set
 211                            .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 212
 213                        let new_hints = this
 214                            .command_hint(snapshot.buffer())
 215                            .into_iter()
 216                            .collect::<Vec<_>>();
 217                        let has_new_hint = !new_hints.is_empty();
 218                        editor.splice_inlays(
 219                            if has_hint {
 220                                &[COMMAND_HINT_INLAY_ID]
 221                            } else {
 222                                &[]
 223                            },
 224                            new_hints,
 225                            cx,
 226                        );
 227                        has_hint = has_new_hint;
 228                    });
 229                    cx.notify();
 230                }
 231            }
 232        }));
 233
 234        Self {
 235            editor,
 236            mention_set,
 237            workspace,
 238            prompt_capabilities,
 239            available_commands,
 240            agent_id,
 241            thread_store,
 242            _subscriptions: subscriptions,
 243            _parse_slash_command_task: Task::ready(()),
 244        }
 245    }
 246
 247    pub fn set_command_state(
 248        &mut self,
 249        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
 250        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
 251        _cx: &mut Context<Self>,
 252    ) {
 253        self.prompt_capabilities = prompt_capabilities;
 254        self.available_commands = available_commands;
 255    }
 256
 257    fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
 258        let available_commands = self.available_commands.borrow();
 259        if available_commands.is_empty() {
 260            return None;
 261        }
 262
 263        let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
 264        if parsed_command.argument.is_some() {
 265            return None;
 266        }
 267
 268        let command_name = parsed_command.command?;
 269        let available_command = available_commands
 270            .iter()
 271            .find(|command| command.name == command_name)?;
 272
 273        let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
 274            mut hint,
 275            ..
 276        }) = available_command.input.clone()?
 277        else {
 278            return None;
 279        };
 280
 281        let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
 282        if hint_pos > snapshot.len() {
 283            hint_pos = snapshot.len();
 284            hint.insert(0, ' ');
 285        }
 286
 287        let hint_pos = snapshot.anchor_after(hint_pos);
 288
 289        Some(Inlay::hint(
 290            COMMAND_HINT_INLAY_ID,
 291            hint_pos,
 292            &InlayHint {
 293                position: hint_pos.text_anchor,
 294                label: InlayHintLabel::String(hint),
 295                kind: Some(InlayHintKind::Parameter),
 296                padding_left: false,
 297                padding_right: false,
 298                tooltip: None,
 299                resolve_state: project::ResolveState::Resolved,
 300            },
 301        ))
 302    }
 303
 304    pub fn insert_thread_summary(
 305        &mut self,
 306        session_id: acp::SessionId,
 307        title: Option<SharedString>,
 308        window: &mut Window,
 309        cx: &mut Context<Self>,
 310    ) {
 311        if self.thread_store.is_none() {
 312            return;
 313        }
 314        let Some(workspace) = self.workspace.upgrade() else {
 315            return;
 316        };
 317        let thread_title = title
 318            .filter(|title| !title.is_empty())
 319            .unwrap_or_else(|| SharedString::new_static("New Thread"));
 320        let uri = MentionUri::Thread {
 321            id: session_id,
 322            name: thread_title.to_string(),
 323        };
 324        let content = format!("{}\n", uri.as_link());
 325
 326        let content_len = content.len() - 1;
 327
 328        let start = self.editor.update(cx, |editor, cx| {
 329            editor.set_text(content, window, cx);
 330            editor
 331                .buffer()
 332                .read(cx)
 333                .snapshot(cx)
 334                .anchor_before(Point::zero())
 335                .text_anchor
 336        });
 337
 338        let supports_images = self.prompt_capabilities.borrow().image;
 339
 340        self.mention_set
 341            .update(cx, |mention_set, cx| {
 342                mention_set.confirm_mention_completion(
 343                    thread_title,
 344                    start,
 345                    content_len,
 346                    uri,
 347                    supports_images,
 348                    self.editor.clone(),
 349                    &workspace,
 350                    window,
 351                    cx,
 352                )
 353            })
 354            .detach();
 355    }
 356
 357    #[cfg(test)]
 358    pub(crate) fn editor(&self) -> &Entity<Editor> {
 359        &self.editor
 360    }
 361
 362    pub fn is_empty(&self, cx: &App) -> bool {
 363        self.editor.read(cx).is_empty(cx)
 364    }
 365
 366    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 367        self.editor
 368            .read(cx)
 369            .context_menu()
 370            .borrow()
 371            .as_ref()
 372            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 373    }
 374
 375    #[cfg(test)]
 376    pub fn mention_set(&self) -> &Entity<MentionSet> {
 377        &self.mention_set
 378    }
 379
 380    fn validate_slash_commands(
 381        text: &str,
 382        available_commands: &[acp::AvailableCommand],
 383        agent_id: &AgentId,
 384    ) -> Result<()> {
 385        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
 386            if let Some(command_name) = parsed_command.command {
 387                // Check if this command is in the list of available commands from the server
 388                let is_supported = available_commands
 389                    .iter()
 390                    .any(|cmd| cmd.name == command_name);
 391
 392                if !is_supported {
 393                    return Err(anyhow!(
 394                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",
 395                        command_name,
 396                        agent_id,
 397                        if available_commands.is_empty() {
 398                            "none".to_string()
 399                        } else {
 400                            available_commands
 401                                .iter()
 402                                .map(|cmd| format!("/{}", cmd.name))
 403                                .collect::<Vec<_>>()
 404                                .join(", ")
 405                        }
 406                    ));
 407                }
 408            }
 409        }
 410        Ok(())
 411    }
 412
 413    pub fn contents(
 414        &self,
 415        full_mention_content: bool,
 416        cx: &mut Context<Self>,
 417    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 418        let text = self.editor.read(cx).text(cx);
 419        let available_commands = self.available_commands.borrow().clone();
 420        let agent_id = self.agent_id.clone();
 421        let build_task = self.build_content_blocks(full_mention_content, cx);
 422
 423        cx.spawn(async move |_, _cx| {
 424            Self::validate_slash_commands(&text, &available_commands, &agent_id)?;
 425            build_task.await
 426        })
 427    }
 428
 429    pub fn draft_contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> {
 430        let build_task = self.build_content_blocks(false, cx);
 431        cx.spawn(async move |_, _cx| {
 432            let (blocks, _tracked_buffers) = build_task.await?;
 433            Ok(blocks)
 434        })
 435    }
 436
 437    fn build_content_blocks(
 438        &self,
 439        full_mention_content: bool,
 440        cx: &mut Context<Self>,
 441    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
 442        let contents = self
 443            .mention_set
 444            .update(cx, |store, cx| store.contents(full_mention_content, cx));
 445        let editor = self.editor.clone();
 446        let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
 447
 448        cx.spawn(async move |_, cx| {
 449            let contents = contents.await?;
 450            let mut all_tracked_buffers = Vec::new();
 451
 452            let result = editor.update(cx, |editor, cx| {
 453                let text = editor.text(cx);
 454                let (mut ix, _) = text
 455                    .char_indices()
 456                    .find(|(_, c)| !c.is_whitespace())
 457                    .unwrap_or((0, '\0'));
 458                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 459                editor.display_map.update(cx, |map, cx| {
 460                    let snapshot = map.snapshot(cx);
 461                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 462                        let Some((uri, mention)) = contents.get(&crease_id) else {
 463                            continue;
 464                        };
 465
 466                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
 467                        if crease_range.start.0 > ix {
 468                            let chunk = text[ix..crease_range.start.0].into();
 469                            chunks.push(chunk);
 470                        }
 471                        let chunk = match mention {
 472                            Mention::Text {
 473                                content,
 474                                tracked_buffers,
 475                            } => {
 476                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
 477                                if supports_embedded_context {
 478                                    acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 479                                        acp::EmbeddedResourceResource::TextResourceContents(
 480                                            acp::TextResourceContents::new(
 481                                                content.clone(),
 482                                                uri.to_uri().to_string(),
 483                                            ),
 484                                        ),
 485                                    ))
 486                                } else {
 487                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
 488                                        uri.name(),
 489                                        uri.to_uri().to_string(),
 490                                    ))
 491                                }
 492                            }
 493                            Mention::Image(mention_image) => acp::ContentBlock::Image(
 494                                acp::ImageContent::new(
 495                                    mention_image.data.clone(),
 496                                    mention_image.format.mime_type(),
 497                                )
 498                                .uri(match uri {
 499                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 500                                    MentionUri::PastedImage => None,
 501                                    other => {
 502                                        debug_panic!(
 503                                            "unexpected mention uri for image: {:?}",
 504                                            other
 505                                        );
 506                                        None
 507                                    }
 508                                }),
 509                            ),
 510                            Mention::Link => acp::ContentBlock::ResourceLink(
 511                                acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
 512                            ),
 513                        };
 514                        chunks.push(chunk);
 515                        ix = crease_range.end.0;
 516                    }
 517
 518                    if ix < text.len() {
 519                        let last_chunk = text[ix..].trim_end().to_owned();
 520                        if !last_chunk.is_empty() {
 521                            chunks.push(last_chunk.into());
 522                        }
 523                    }
 524                });
 525                anyhow::Ok((chunks, all_tracked_buffers))
 526            })?;
 527            Ok(result)
 528        })
 529    }
 530
 531    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 532        self.editor.update(cx, |editor, cx| {
 533            editor.clear(window, cx);
 534            editor.remove_creases(
 535                self.mention_set.update(cx, |mention_set, _cx| {
 536                    mention_set
 537                        .clear()
 538                        .map(|(crease_id, _)| crease_id)
 539                        .collect::<Vec<_>>()
 540                }),
 541                cx,
 542            )
 543        });
 544    }
 545
 546    pub fn send(&mut self, cx: &mut Context<Self>) {
 547        if !self.is_empty(cx) {
 548            self.editor.update(cx, |editor, cx| {
 549                editor.clear_inlay_hints(cx);
 550            });
 551        }
 552        cx.emit(MessageEditorEvent::Send)
 553    }
 554
 555    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 556        self.insert_context_prefix("@", window, cx);
 557    }
 558
 559    pub fn insert_context_type(
 560        &mut self,
 561        context_keyword: &str,
 562        window: &mut Window,
 563        cx: &mut Context<Self>,
 564    ) {
 565        let prefix = format!("@{}", context_keyword);
 566        self.insert_context_prefix(&prefix, window, cx);
 567    }
 568
 569    fn insert_context_prefix(&mut self, prefix: &str, window: &mut Window, cx: &mut Context<Self>) {
 570        let editor = self.editor.clone();
 571        let prefix = prefix.to_string();
 572
 573        cx.spawn_in(window, async move |_, cx| {
 574            editor
 575                .update_in(cx, |editor, window, cx| {
 576                    let menu_is_open =
 577                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 578                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 579                        });
 580
 581                    let has_prefix = {
 582                        let snapshot = editor.display_snapshot(cx);
 583                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 584                        let offset = cursor.to_offset(&snapshot);
 585                        let buffer_snapshot = snapshot.buffer_snapshot();
 586                        let prefix_char_count = prefix.chars().count();
 587                        buffer_snapshot
 588                            .reversed_chars_at(offset)
 589                            .take(prefix_char_count)
 590                            .eq(prefix.chars().rev())
 591                    };
 592
 593                    if menu_is_open && has_prefix {
 594                        return;
 595                    }
 596
 597                    editor.insert(&prefix, window, cx);
 598                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 599                })
 600                .log_err();
 601        })
 602        .detach();
 603    }
 604
 605    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 606        self.send(cx);
 607    }
 608
 609    fn send_immediately(&mut self, _: &SendImmediately, _: &mut Window, cx: &mut Context<Self>) {
 610        if self.is_empty(cx) {
 611            return;
 612        }
 613
 614        self.editor.update(cx, |editor, cx| {
 615            editor.clear_inlay_hints(cx);
 616        });
 617
 618        cx.emit(MessageEditorEvent::SendImmediately)
 619    }
 620
 621    fn chat_with_follow(
 622        &mut self,
 623        _: &ChatWithFollow,
 624        window: &mut Window,
 625        cx: &mut Context<Self>,
 626    ) {
 627        self.workspace
 628            .update(cx, |this, cx| {
 629                this.follow(CollaboratorId::Agent, window, cx)
 630            })
 631            .log_err();
 632
 633        self.send(cx);
 634    }
 635
 636    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 637        cx.emit(MessageEditorEvent::Cancel)
 638    }
 639
 640    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 641        let Some(workspace) = self.workspace.upgrade() else {
 642            return;
 643        };
 644        let editor_clipboard_selections = cx
 645            .read_from_clipboard()
 646            .and_then(|item| item.entries().first().cloned())
 647            .and_then(|entry| match entry {
 648                ClipboardEntry::String(text) => {
 649                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
 650                }
 651                _ => None,
 652            });
 653
 654        // Insert creases for pasted clipboard selections that:
 655        // 1. Contain exactly one selection
 656        // 2. Have an associated file path
 657        // 3. Span multiple lines (not single-line selections)
 658        // 4. Belong to a file that exists in the current project
 659        let should_insert_creases = util::maybe!({
 660            let selections = editor_clipboard_selections.as_ref()?;
 661            if selections.len() > 1 {
 662                return Some(false);
 663            }
 664            let selection = selections.first()?;
 665            let file_path = selection.file_path.as_ref()?;
 666            let line_range = selection.line_range.as_ref()?;
 667
 668            if line_range.start() == line_range.end() {
 669                return Some(false);
 670            }
 671
 672            Some(
 673                workspace
 674                    .read(cx)
 675                    .project()
 676                    .read(cx)
 677                    .project_path_for_absolute_path(file_path, cx)
 678                    .is_some(),
 679            )
 680        })
 681        .unwrap_or(false);
 682
 683        if should_insert_creases && let Some(selections) = editor_clipboard_selections {
 684            cx.stop_propagation();
 685            let insertion_target = self
 686                .editor
 687                .read(cx)
 688                .selections
 689                .newest_anchor()
 690                .start
 691                .text_anchor;
 692
 693            let project = workspace.read(cx).project().clone();
 694            for selection in selections {
 695                if let (Some(file_path), Some(line_range)) =
 696                    (selection.file_path, selection.line_range)
 697                {
 698                    let crease_text =
 699                        acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
 700
 701                    let mention_uri = MentionUri::Selection {
 702                        abs_path: Some(file_path.clone()),
 703                        line_range: line_range.clone(),
 704                    };
 705
 706                    let mention_text = mention_uri.as_link().to_string();
 707                    let (excerpt_id, text_anchor, content_len) =
 708                        self.editor.update(cx, |editor, cx| {
 709                            let buffer = editor.buffer().read(cx);
 710                            let snapshot = buffer.snapshot(cx);
 711                            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
 712                            let text_anchor = insertion_target.bias_left(&buffer_snapshot);
 713
 714                            editor.insert(&mention_text, window, cx);
 715                            editor.insert(" ", window, cx);
 716
 717                            (excerpt_id, text_anchor, mention_text.len())
 718                        });
 719
 720                    let Some((crease_id, tx)) = insert_crease_for_mention(
 721                        excerpt_id,
 722                        text_anchor,
 723                        content_len,
 724                        crease_text.into(),
 725                        mention_uri.icon_path(cx),
 726                        mention_uri.tooltip_text(),
 727                        Some(mention_uri.clone()),
 728                        Some(self.workspace.clone()),
 729                        None,
 730                        self.editor.clone(),
 731                        window,
 732                        cx,
 733                    ) else {
 734                        continue;
 735                    };
 736                    drop(tx);
 737
 738                    let mention_task = cx
 739                        .spawn({
 740                            let project = project.clone();
 741                            async move |_, cx| {
 742                                let project_path = project
 743                                    .update(cx, |project, cx| {
 744                                        project.project_path_for_absolute_path(&file_path, cx)
 745                                    })
 746                                    .ok_or_else(|| "project path not found".to_string())?;
 747
 748                                let buffer = project
 749                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
 750                                    .await
 751                                    .map_err(|e| e.to_string())?;
 752
 753                                Ok(buffer.update(cx, |buffer, cx| {
 754                                    let start =
 755                                        Point::new(*line_range.start(), 0).min(buffer.max_point());
 756                                    let end = Point::new(*line_range.end() + 1, 0)
 757                                        .min(buffer.max_point());
 758                                    let content = buffer.text_for_range(start..end).collect();
 759                                    Mention::Text {
 760                                        content,
 761                                        tracked_buffers: vec![cx.entity()],
 762                                    }
 763                                }))
 764                            }
 765                        })
 766                        .shared();
 767
 768                    self.mention_set.update(cx, |mention_set, _cx| {
 769                        mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
 770                    });
 771                }
 772            }
 773            return;
 774        }
 775        // Handle text paste with potential markdown mention links.
 776        // This must be checked BEFORE paste_images_as_context because that function
 777        // returns a task even when there are no images in the clipboard.
 778        if let Some(clipboard_text) = cx
 779            .read_from_clipboard()
 780            .and_then(|item| item.entries().first().cloned())
 781            .and_then(|entry| match entry {
 782                ClipboardEntry::String(text) => Some(text.text().to_string()),
 783                _ => None,
 784            })
 785        {
 786            if clipboard_text.contains("[@") {
 787                cx.stop_propagation();
 788                let selections_before = self.editor.update(cx, |editor, cx| {
 789                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 790                    editor
 791                        .selections
 792                        .disjoint_anchors()
 793                        .iter()
 794                        .map(|selection| {
 795                            (
 796                                selection.start.bias_left(&snapshot),
 797                                selection.end.bias_right(&snapshot),
 798                            )
 799                        })
 800                        .collect::<Vec<_>>()
 801                });
 802
 803                self.editor.update(cx, |editor, cx| {
 804                    editor.insert(&clipboard_text, window, cx);
 805                });
 806
 807                let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
 808                let path_style = workspace.read(cx).project().read(cx).path_style(cx);
 809
 810                let mut all_mentions = Vec::new();
 811                for (start_anchor, end_anchor) in selections_before {
 812                    let start_offset = start_anchor.to_offset(&snapshot);
 813                    let end_offset = end_anchor.to_offset(&snapshot);
 814
 815                    // Get the actual inserted text from the buffer (may differ due to auto-indent)
 816                    let inserted_text: String =
 817                        snapshot.text_for_range(start_offset..end_offset).collect();
 818
 819                    let parsed_mentions = parse_mention_links(&inserted_text, path_style);
 820                    for (range, mention_uri) in parsed_mentions {
 821                        let mention_start_offset = MultiBufferOffset(start_offset.0 + range.start);
 822                        let anchor = snapshot.anchor_before(mention_start_offset);
 823                        let content_len = range.end - range.start;
 824                        all_mentions.push((anchor, content_len, mention_uri));
 825                    }
 826                }
 827
 828                if !all_mentions.is_empty() {
 829                    let supports_images = self.prompt_capabilities.borrow().image;
 830                    let http_client = workspace.read(cx).client().http_client();
 831
 832                    for (anchor, content_len, mention_uri) in all_mentions {
 833                        let Some((crease_id, tx)) = insert_crease_for_mention(
 834                            anchor.excerpt_id,
 835                            anchor.text_anchor,
 836                            content_len,
 837                            mention_uri.name().into(),
 838                            mention_uri.icon_path(cx),
 839                            mention_uri.tooltip_text(),
 840                            Some(mention_uri.clone()),
 841                            Some(self.workspace.clone()),
 842                            None,
 843                            self.editor.clone(),
 844                            window,
 845                            cx,
 846                        ) else {
 847                            continue;
 848                        };
 849
 850                        // Create the confirmation task based on the mention URI type.
 851                        // This properly loads file content, fetches URLs, etc.
 852                        let task = self.mention_set.update(cx, |mention_set, cx| {
 853                            mention_set.confirm_mention_for_uri(
 854                                mention_uri.clone(),
 855                                supports_images,
 856                                http_client.clone(),
 857                                cx,
 858                            )
 859                        });
 860                        let task = cx
 861                            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
 862                            .shared();
 863
 864                        self.mention_set.update(cx, |mention_set, _cx| {
 865                            mention_set.insert_mention(crease_id, mention_uri.clone(), task.clone())
 866                        });
 867
 868                        // Drop the tx after inserting to signal the crease is ready
 869                        drop(tx);
 870                    }
 871                    return;
 872                }
 873            }
 874        }
 875
 876        if self.prompt_capabilities.borrow().image
 877            && let Some(task) = paste_images_as_context(
 878                self.editor.clone(),
 879                self.mention_set.clone(),
 880                self.workspace.clone(),
 881                window,
 882                cx,
 883            )
 884        {
 885            task.detach();
 886            return;
 887        }
 888
 889        // Fall through to default editor paste
 890        cx.propagate();
 891    }
 892
 893    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
 894        let editor = self.editor.clone();
 895        window.defer(cx, move |window, cx| {
 896            editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
 897        });
 898    }
 899
 900    pub fn insert_dragged_files(
 901        &mut self,
 902        paths: Vec<project::ProjectPath>,
 903        added_worktrees: Vec<Entity<Worktree>>,
 904        window: &mut Window,
 905        cx: &mut Context<Self>,
 906    ) {
 907        let Some(workspace) = self.workspace.upgrade() else {
 908            return;
 909        };
 910        let project = workspace.read(cx).project().clone();
 911        let path_style = project.read(cx).path_style(cx);
 912        let buffer = self.editor.read(cx).buffer().clone();
 913        let Some(buffer) = buffer.read(cx).as_singleton() else {
 914            return;
 915        };
 916        let mut tasks = Vec::new();
 917        for path in paths {
 918            let Some(entry) = project.read(cx).entry_for_path(&path, cx) else {
 919                continue;
 920            };
 921            let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 922                continue;
 923            };
 924            let abs_path = worktree.read(cx).absolutize(&path.path);
 925            let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 926                &path.path,
 927                worktree.read(cx).root_name(),
 928                path_style,
 929            );
 930
 931            let uri = if entry.is_dir() {
 932                MentionUri::Directory { abs_path }
 933            } else {
 934                MentionUri::File { abs_path }
 935            };
 936
 937            let new_text = format!("{} ", uri.as_link());
 938            let content_len = new_text.len() - 1;
 939
 940            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 941
 942            self.editor.update(cx, |message_editor, cx| {
 943                message_editor.edit(
 944                    [(
 945                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 946                        new_text,
 947                    )],
 948                    cx,
 949                );
 950            });
 951            let supports_images = self.prompt_capabilities.borrow().image;
 952            tasks.push(self.mention_set.update(cx, |mention_set, cx| {
 953                mention_set.confirm_mention_completion(
 954                    file_name,
 955                    anchor,
 956                    content_len,
 957                    uri,
 958                    supports_images,
 959                    self.editor.clone(),
 960                    &workspace,
 961                    window,
 962                    cx,
 963                )
 964            }));
 965        }
 966        cx.spawn(async move |_, _| {
 967            join_all(tasks).await;
 968            drop(added_worktrees);
 969        })
 970        .detach();
 971    }
 972
 973    /// Inserts code snippets as creases into the editor.
 974    /// Each tuple contains (code_text, crease_title).
 975    pub fn insert_code_creases(
 976        &mut self,
 977        creases: Vec<(String, String)>,
 978        window: &mut Window,
 979        cx: &mut Context<Self>,
 980    ) {
 981        self.editor.update(cx, |editor, cx| {
 982            editor.insert("\n", window, cx);
 983        });
 984        for (text, crease_title) in creases {
 985            self.insert_crease_impl(text, crease_title, IconName::TextSnippet, true, window, cx);
 986        }
 987    }
 988
 989    pub fn insert_terminal_crease(
 990        &mut self,
 991        text: String,
 992        window: &mut Window,
 993        cx: &mut Context<Self>,
 994    ) {
 995        let line_count = text.lines().count() as u32;
 996        let mention_uri = MentionUri::TerminalSelection { line_count };
 997        let mention_text = mention_uri.as_link().to_string();
 998
 999        let (excerpt_id, text_anchor, content_len) = self.editor.update(cx, |editor, cx| {
1000            let buffer = editor.buffer().read(cx);
1001            let snapshot = buffer.snapshot(cx);
1002            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
1003            let text_anchor = editor
1004                .selections
1005                .newest_anchor()
1006                .start
1007                .text_anchor
1008                .bias_left(&buffer_snapshot);
1009
1010            editor.insert(&mention_text, window, cx);
1011            editor.insert(" ", window, cx);
1012
1013            (excerpt_id, text_anchor, mention_text.len())
1014        });
1015
1016        let Some((crease_id, tx)) = insert_crease_for_mention(
1017            excerpt_id,
1018            text_anchor,
1019            content_len,
1020            mention_uri.name().into(),
1021            mention_uri.icon_path(cx),
1022            mention_uri.tooltip_text(),
1023            Some(mention_uri.clone()),
1024            Some(self.workspace.clone()),
1025            None,
1026            self.editor.clone(),
1027            window,
1028            cx,
1029        ) else {
1030            return;
1031        };
1032        drop(tx);
1033
1034        let mention_task = Task::ready(Ok(Mention::Text {
1035            content: text,
1036            tracked_buffers: vec![],
1037        }))
1038        .shared();
1039
1040        self.mention_set.update(cx, |mention_set, _| {
1041            mention_set.insert_mention(crease_id, mention_uri, mention_task);
1042        });
1043    }
1044
1045    pub fn insert_branch_diff_crease(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1046        let Some(workspace) = self.workspace.upgrade() else {
1047            return;
1048        };
1049
1050        let project = workspace.read(cx).project().clone();
1051
1052        let Some(repo) = project.read(cx).active_repository(cx) else {
1053            return;
1054        };
1055
1056        let default_branch_receiver = repo.update(cx, |repo, _| repo.default_branch(false));
1057        let editor = self.editor.clone();
1058        let mention_set = self.mention_set.clone();
1059        let weak_workspace = self.workspace.clone();
1060
1061        window
1062            .spawn(cx, async move |cx| {
1063                let base_ref: SharedString = default_branch_receiver
1064                    .await
1065                    .ok()
1066                    .and_then(|r| r.ok())
1067                    .flatten()
1068                    .ok_or_else(|| anyhow!("Could not determine default branch"))?;
1069
1070                cx.update(|window, cx| {
1071                    let mention_uri = MentionUri::GitDiff {
1072                        base_ref: base_ref.to_string(),
1073                    };
1074                    let mention_text = mention_uri.as_link().to_string();
1075
1076                    let (excerpt_id, text_anchor, content_len) = editor.update(cx, |editor, cx| {
1077                        let buffer = editor.buffer().read(cx);
1078                        let snapshot = buffer.snapshot(cx);
1079                        let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
1080                        let text_anchor = editor
1081                            .selections
1082                            .newest_anchor()
1083                            .start
1084                            .text_anchor
1085                            .bias_left(&buffer_snapshot);
1086
1087                        editor.insert(&mention_text, window, cx);
1088                        editor.insert(" ", window, cx);
1089
1090                        (excerpt_id, text_anchor, mention_text.len())
1091                    });
1092
1093                    let Some((crease_id, tx)) = insert_crease_for_mention(
1094                        excerpt_id,
1095                        text_anchor,
1096                        content_len,
1097                        mention_uri.name().into(),
1098                        mention_uri.icon_path(cx),
1099                        mention_uri.tooltip_text(),
1100                        Some(mention_uri.clone()),
1101                        Some(weak_workspace),
1102                        None,
1103                        editor,
1104                        window,
1105                        cx,
1106                    ) else {
1107                        return;
1108                    };
1109                    drop(tx);
1110
1111                    let confirm_task = mention_set.update(cx, |mention_set, cx| {
1112                        mention_set.confirm_mention_for_git_diff(base_ref, cx)
1113                    });
1114
1115                    let mention_task = cx
1116                        .spawn(async move |_cx| confirm_task.await.map_err(|e| e.to_string()))
1117                        .shared();
1118
1119                    mention_set.update(cx, |mention_set, _| {
1120                        mention_set.insert_mention(crease_id, mention_uri, mention_task);
1121                    });
1122                })
1123            })
1124            .detach_and_log_err(cx);
1125    }
1126
1127    fn insert_crease_impl(
1128        &mut self,
1129        text: String,
1130        title: String,
1131        icon: IconName,
1132        add_trailing_newline: bool,
1133        window: &mut Window,
1134        cx: &mut Context<Self>,
1135    ) {
1136        use editor::display_map::{Crease, FoldPlaceholder};
1137        use multi_buffer::MultiBufferRow;
1138        use rope::Point;
1139
1140        self.editor.update(cx, |editor, cx| {
1141            let point = editor
1142                .selections
1143                .newest::<Point>(&editor.display_snapshot(cx))
1144                .head();
1145            let start_row = MultiBufferRow(point.row);
1146
1147            editor.insert(&text, window, cx);
1148
1149            let snapshot = editor.buffer().read(cx).snapshot(cx);
1150            let anchor_before = snapshot.anchor_after(point);
1151            let anchor_after = editor
1152                .selections
1153                .newest_anchor()
1154                .head()
1155                .bias_left(&snapshot);
1156
1157            if add_trailing_newline {
1158                editor.insert("\n", window, cx);
1159            }
1160
1161            let fold_placeholder = FoldPlaceholder {
1162                render: Arc::new({
1163                    let title = title.clone();
1164                    move |_fold_id, _fold_range, _cx| {
1165                        Button::new("crease", title.clone())
1166                            .layer(ElevationIndex::ElevatedSurface)
1167                            .start_icon(Icon::new(icon))
1168                            .into_any_element()
1169                    }
1170                }),
1171                merge_adjacent: false,
1172                ..Default::default()
1173            };
1174
1175            let crease = Crease::inline(
1176                anchor_before..anchor_after,
1177                fold_placeholder,
1178                |row, is_folded, fold, _window, _cx| {
1179                    Disclosure::new(("crease-toggle", row.0 as u64), !is_folded)
1180                        .toggle_state(is_folded)
1181                        .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
1182                        .into_any_element()
1183                },
1184                |_, _, _, _| gpui::Empty.into_any(),
1185            );
1186            editor.insert_creases(vec![crease], cx);
1187            editor.fold_at(start_row, window, cx);
1188        });
1189    }
1190
1191    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1192        let editor = self.editor.read(cx);
1193        let editor_buffer = editor.buffer().read(cx);
1194        let Some(buffer) = editor_buffer.as_singleton() else {
1195            return;
1196        };
1197        let cursor_anchor = editor.selections.newest_anchor().head();
1198        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
1199        let anchor = buffer.update(cx, |buffer, _cx| {
1200            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
1201        });
1202        let Some(workspace) = self.workspace.upgrade() else {
1203            return;
1204        };
1205        let Some(completion) =
1206            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
1207                PromptContextAction::AddSelections,
1208                anchor..anchor,
1209                self.editor.downgrade(),
1210                self.mention_set.downgrade(),
1211                &workspace,
1212                cx,
1213            )
1214        else {
1215            return;
1216        };
1217
1218        self.editor.update(cx, |message_editor, cx| {
1219            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
1220            message_editor.request_autoscroll(Autoscroll::fit(), cx);
1221        });
1222        if let Some(confirm) = completion.confirm {
1223            confirm(CompletionIntent::Complete, window, cx);
1224        }
1225    }
1226
1227    pub fn add_images_from_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1228        if !self.prompt_capabilities.borrow().image {
1229            return;
1230        }
1231
1232        let editor = self.editor.clone();
1233        let mention_set = self.mention_set.clone();
1234        let workspace = self.workspace.clone();
1235
1236        let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
1237            files: true,
1238            directories: false,
1239            multiple: true,
1240            prompt: Some("Select Images".into()),
1241        });
1242
1243        window
1244            .spawn(cx, async move |cx| {
1245                let paths = match paths_receiver.await {
1246                    Ok(Ok(Some(paths))) => paths,
1247                    _ => return Ok::<(), anyhow::Error>(()),
1248                };
1249
1250                let supported_formats = [
1251                    ("png", gpui::ImageFormat::Png),
1252                    ("jpg", gpui::ImageFormat::Jpeg),
1253                    ("jpeg", gpui::ImageFormat::Jpeg),
1254                    ("webp", gpui::ImageFormat::Webp),
1255                    ("gif", gpui::ImageFormat::Gif),
1256                    ("bmp", gpui::ImageFormat::Bmp),
1257                    ("tiff", gpui::ImageFormat::Tiff),
1258                    ("tif", gpui::ImageFormat::Tiff),
1259                    ("ico", gpui::ImageFormat::Ico),
1260                ];
1261
1262                let mut images = Vec::new();
1263                for path in paths {
1264                    let extension = path
1265                        .extension()
1266                        .and_then(|ext| ext.to_str())
1267                        .map(|s| s.to_lowercase());
1268
1269                    let Some(format) = extension.and_then(|ext| {
1270                        supported_formats
1271                            .iter()
1272                            .find(|(e, _)| *e == ext)
1273                            .map(|(_, f)| *f)
1274                    }) else {
1275                        continue;
1276                    };
1277
1278                    let Ok(content) = async_fs::read(&path).await else {
1279                        continue;
1280                    };
1281
1282                    images.push(gpui::Image::from_bytes(format, content));
1283                }
1284
1285                crate::mention_set::insert_images_as_context(
1286                    images,
1287                    editor,
1288                    mention_set,
1289                    workspace,
1290                    cx,
1291                )
1292                .await;
1293                Ok(())
1294            })
1295            .detach_and_log_err(cx);
1296    }
1297
1298    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1299        self.editor.update(cx, |message_editor, cx| {
1300            message_editor.set_read_only(read_only);
1301            cx.notify()
1302        })
1303    }
1304
1305    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1306        self.editor.update(cx, |editor, cx| {
1307            if *editor.mode() != mode {
1308                editor.set_mode(mode);
1309                cx.notify()
1310            }
1311        });
1312    }
1313
1314    pub fn set_message(
1315        &mut self,
1316        message: Vec<acp::ContentBlock>,
1317        window: &mut Window,
1318        cx: &mut Context<Self>,
1319    ) {
1320        self.clear(window, cx);
1321        self.insert_message_blocks(message, false, window, cx);
1322    }
1323
1324    pub fn append_message(
1325        &mut self,
1326        message: Vec<acp::ContentBlock>,
1327        separator: Option<&str>,
1328        window: &mut Window,
1329        cx: &mut Context<Self>,
1330    ) {
1331        if message.is_empty() {
1332            return;
1333        }
1334
1335        if let Some(separator) = separator
1336            && !separator.is_empty()
1337            && !self.is_empty(cx)
1338        {
1339            self.editor.update(cx, |editor, cx| {
1340                editor.insert(separator, window, cx);
1341            });
1342        }
1343
1344        self.insert_message_blocks(message, true, window, cx);
1345    }
1346
1347    fn insert_message_blocks(
1348        &mut self,
1349        message: Vec<acp::ContentBlock>,
1350        append_to_existing: bool,
1351        window: &mut Window,
1352        cx: &mut Context<Self>,
1353    ) {
1354        let Some(workspace) = self.workspace.upgrade() else {
1355            return;
1356        };
1357
1358        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
1359        let mut text = String::new();
1360        let mut mentions = Vec::new();
1361
1362        for chunk in message {
1363            match chunk {
1364                acp::ContentBlock::Text(text_content) => {
1365                    text.push_str(&text_content.text);
1366                }
1367                acp::ContentBlock::Resource(acp::EmbeddedResource {
1368                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1369                    ..
1370                }) => {
1371                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
1372                    else {
1373                        continue;
1374                    };
1375                    let start = text.len();
1376                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1377                    let end = text.len();
1378                    mentions.push((
1379                        start..end,
1380                        mention_uri,
1381                        Mention::Text {
1382                            content: resource.text,
1383                            tracked_buffers: Vec::new(),
1384                        },
1385                    ));
1386                }
1387                acp::ContentBlock::ResourceLink(resource) => {
1388                    if let Some(mention_uri) =
1389                        MentionUri::parse(&resource.uri, path_style).log_err()
1390                    {
1391                        let start = text.len();
1392                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1393                        let end = text.len();
1394                        mentions.push((start..end, mention_uri, Mention::Link));
1395                    }
1396                }
1397                acp::ContentBlock::Image(acp::ImageContent {
1398                    uri,
1399                    data,
1400                    mime_type,
1401                    ..
1402                }) => {
1403                    let mention_uri = if let Some(uri) = uri {
1404                        MentionUri::parse(&uri, path_style)
1405                    } else {
1406                        Ok(MentionUri::PastedImage)
1407                    };
1408                    let Some(mention_uri) = mention_uri.log_err() else {
1409                        continue;
1410                    };
1411                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1412                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1413                        continue;
1414                    };
1415                    let start = text.len();
1416                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1417                    let end = text.len();
1418                    mentions.push((
1419                        start..end,
1420                        mention_uri,
1421                        Mention::Image(MentionImage {
1422                            data: data.into(),
1423                            format,
1424                        }),
1425                    ));
1426                }
1427                _ => {}
1428            }
1429        }
1430
1431        if text.is_empty() && mentions.is_empty() {
1432            return;
1433        }
1434
1435        let insertion_start = if append_to_existing {
1436            self.editor.read(cx).text(cx).len()
1437        } else {
1438            0
1439        };
1440
1441        let snapshot = if append_to_existing {
1442            self.editor.update(cx, |editor, cx| {
1443                editor.insert(&text, window, cx);
1444                editor.buffer().read(cx).snapshot(cx)
1445            })
1446        } else {
1447            self.editor.update(cx, |editor, cx| {
1448                editor.set_text(text, window, cx);
1449                editor.buffer().read(cx).snapshot(cx)
1450            })
1451        };
1452
1453        for (range, mention_uri, mention) in mentions {
1454            let adjusted_start = insertion_start + range.start;
1455            let anchor = snapshot.anchor_before(MultiBufferOffset(adjusted_start));
1456            let Some((crease_id, tx)) = insert_crease_for_mention(
1457                anchor.excerpt_id,
1458                anchor.text_anchor,
1459                range.end - range.start,
1460                mention_uri.name().into(),
1461                mention_uri.icon_path(cx),
1462                mention_uri.tooltip_text(),
1463                Some(mention_uri.clone()),
1464                Some(self.workspace.clone()),
1465                None,
1466                self.editor.clone(),
1467                window,
1468                cx,
1469            ) else {
1470                continue;
1471            };
1472            drop(tx);
1473
1474            self.mention_set.update(cx, |mention_set, _cx| {
1475                mention_set.insert_mention(
1476                    crease_id,
1477                    mention_uri.clone(),
1478                    Task::ready(Ok(mention)).shared(),
1479                )
1480            });
1481        }
1482
1483        cx.notify();
1484    }
1485
1486    pub fn text(&self, cx: &App) -> String {
1487        self.editor.read(cx).text(cx)
1488    }
1489
1490    pub fn insert_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1491        if text.is_empty() {
1492            return;
1493        }
1494
1495        self.editor.update(cx, |editor, cx| {
1496            editor.insert(text, window, cx);
1497        });
1498    }
1499
1500    pub fn set_placeholder_text(
1501        &mut self,
1502        placeholder: &str,
1503        window: &mut Window,
1504        cx: &mut Context<Self>,
1505    ) {
1506        self.editor.update(cx, |editor, cx| {
1507            editor.set_placeholder_text(placeholder, window, cx);
1508        });
1509    }
1510
1511    #[cfg(any(test, feature = "test-support"))]
1512    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1513        self.editor.update(cx, |editor, cx| {
1514            editor.set_text(text, window, cx);
1515        });
1516    }
1517}
1518
1519impl Focusable for MessageEditor {
1520    fn focus_handle(&self, cx: &App) -> FocusHandle {
1521        self.editor.focus_handle(cx)
1522    }
1523}
1524
1525impl Render for MessageEditor {
1526    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1527        div()
1528            .key_context("MessageEditor")
1529            .on_action(cx.listener(Self::chat))
1530            .on_action(cx.listener(Self::send_immediately))
1531            .on_action(cx.listener(Self::chat_with_follow))
1532            .on_action(cx.listener(Self::cancel))
1533            .on_action(cx.listener(Self::paste_raw))
1534            .capture_action(cx.listener(Self::paste))
1535            .flex_1()
1536            .child({
1537                let settings = ThemeSettings::get_global(cx);
1538
1539                let text_style = TextStyle {
1540                    color: cx.theme().colors().text,
1541                    font_family: settings.buffer_font.family.clone(),
1542                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1543                    font_features: settings.buffer_font.features.clone(),
1544                    font_size: settings.agent_buffer_font_size(cx).into(),
1545                    font_weight: settings.buffer_font.weight,
1546                    line_height: relative(settings.buffer_line_height.value()),
1547                    ..Default::default()
1548                };
1549
1550                EditorElement::new(
1551                    &self.editor,
1552                    EditorStyle {
1553                        background: cx.theme().colors().editor_background,
1554                        local_player: cx.theme().players().local(),
1555                        text: text_style,
1556                        syntax: cx.theme().syntax().clone(),
1557                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1558                        ..Default::default()
1559                    },
1560                )
1561            })
1562    }
1563}
1564
1565pub struct MessageEditorAddon {}
1566
1567impl MessageEditorAddon {
1568    pub fn new() -> Self {
1569        Self {}
1570    }
1571}
1572
1573impl Addon for MessageEditorAddon {
1574    fn to_any(&self) -> &dyn std::any::Any {
1575        self
1576    }
1577
1578    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1579        Some(self)
1580    }
1581
1582    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1583        let settings = agent_settings::AgentSettings::get_global(cx);
1584        if settings.use_modifier_to_send {
1585            key_context.add("use_modifier_to_send");
1586        }
1587    }
1588}
1589
1590/// Parses markdown mention links in the format `[@name](uri)` from text.
1591/// Returns a vector of (range, MentionUri) pairs where range is the byte range in the text.
1592fn parse_mention_links(text: &str, path_style: PathStyle) -> Vec<(Range<usize>, MentionUri)> {
1593    let mut mentions = Vec::new();
1594    let mut search_start = 0;
1595
1596    while let Some(link_start) = text[search_start..].find("[@") {
1597        let absolute_start = search_start + link_start;
1598
1599        // Find the matching closing bracket for the name, handling nested brackets.
1600        // Start at the '[' character so find_matching_bracket can track depth correctly.
1601        let Some(name_end) = find_matching_bracket(&text[absolute_start..], '[', ']') else {
1602            search_start = absolute_start + 2;
1603            continue;
1604        };
1605        let name_end = absolute_start + name_end;
1606
1607        // Check for opening parenthesis immediately after
1608        if text.get(name_end + 1..name_end + 2) != Some("(") {
1609            search_start = name_end + 1;
1610            continue;
1611        }
1612
1613        // Find the matching closing parenthesis for the URI, handling nested parens
1614        let uri_start = name_end + 2;
1615        let Some(uri_end_relative) = find_matching_bracket(&text[name_end + 1..], '(', ')') else {
1616            search_start = uri_start;
1617            continue;
1618        };
1619        let uri_end = name_end + 1 + uri_end_relative;
1620        let link_end = uri_end + 1;
1621
1622        let uri_str = &text[uri_start..uri_end];
1623
1624        // Try to parse the URI as a MentionUri
1625        if let Ok(mention_uri) = MentionUri::parse(uri_str, path_style) {
1626            mentions.push((absolute_start..link_end, mention_uri));
1627        }
1628
1629        search_start = link_end;
1630    }
1631
1632    mentions
1633}
1634
1635/// Finds the position of the matching closing bracket, handling nested brackets.
1636/// The input `text` should start with the opening bracket.
1637/// Returns the index of the matching closing bracket relative to `text`.
1638fn find_matching_bracket(text: &str, open: char, close: char) -> Option<usize> {
1639    let mut depth = 0;
1640    for (index, character) in text.char_indices() {
1641        if character == open {
1642            depth += 1;
1643        } else if character == close {
1644            depth -= 1;
1645            if depth == 0 {
1646                return Some(index);
1647            }
1648        }
1649    }
1650    None
1651}
1652
1653#[cfg(test)]
1654mod tests {
1655    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1656
1657    use acp_thread::MentionUri;
1658    use agent::{ThreadStore, outline};
1659    use agent_client_protocol as acp;
1660    use editor::{
1661        AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset, SelectionEffects,
1662        actions::Paste,
1663    };
1664
1665    use fs::FakeFs;
1666    use futures::StreamExt as _;
1667    use gpui::{
1668        AppContext, ClipboardItem, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext,
1669        VisualTestContext,
1670    };
1671    use language_model::LanguageModelRegistry;
1672    use lsp::{CompletionContext, CompletionTriggerKind};
1673    use project::{CompletionIntent, Project, ProjectPath};
1674    use serde_json::json;
1675
1676    use text::Point;
1677    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1678    use util::{path, paths::PathStyle, rel_path::rel_path};
1679    use workspace::{AppState, Item, MultiWorkspace};
1680
1681    use crate::completion_provider::{PromptCompletionProviderDelegate, PromptContextType};
1682    use crate::{
1683        conversation_view::tests::init_test,
1684        message_editor::{Mention, MessageEditor, parse_mention_links},
1685    };
1686
1687    #[test]
1688    fn test_parse_mention_links() {
1689        // Single file mention
1690        let text = "[@bundle-mac](file:///Users/test/zed/script/bundle-mac)";
1691        let mentions = parse_mention_links(text, PathStyle::local());
1692        assert_eq!(mentions.len(), 1);
1693        assert_eq!(mentions[0].0, 0..text.len());
1694        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
1695
1696        // Multiple mentions
1697        let text = "Check [@file1](file:///path/to/file1) and [@file2](file:///path/to/file2)!";
1698        let mentions = parse_mention_links(text, PathStyle::local());
1699        assert_eq!(mentions.len(), 2);
1700
1701        // Text without mentions
1702        let text = "Just some regular text without mentions";
1703        let mentions = parse_mention_links(text, PathStyle::local());
1704        assert_eq!(mentions.len(), 0);
1705
1706        // Malformed mentions (should be skipped)
1707        let text = "[@incomplete](invalid://uri) and [@missing](";
1708        let mentions = parse_mention_links(text, PathStyle::local());
1709        assert_eq!(mentions.len(), 0);
1710
1711        // Mixed content with valid mention
1712        let text = "Before [@valid](file:///path/to/file) after";
1713        let mentions = parse_mention_links(text, PathStyle::local());
1714        assert_eq!(mentions.len(), 1);
1715        assert_eq!(mentions[0].0.start, 7);
1716
1717        // HTTP URL mention (Fetch)
1718        let text = "Check out [@docs](https://example.com/docs) for more info";
1719        let mentions = parse_mention_links(text, PathStyle::local());
1720        assert_eq!(mentions.len(), 1);
1721        assert!(matches!(mentions[0].1, MentionUri::Fetch { .. }));
1722
1723        // Directory mention (trailing slash)
1724        let text = "[@src](file:///path/to/src/)";
1725        let mentions = parse_mention_links(text, PathStyle::local());
1726        assert_eq!(mentions.len(), 1);
1727        assert!(matches!(mentions[0].1, MentionUri::Directory { .. }));
1728
1729        // Multiple different mention types
1730        let text = "File [@f](file:///a) and URL [@u](https://b.com) and dir [@d](file:///c/)";
1731        let mentions = parse_mention_links(text, PathStyle::local());
1732        assert_eq!(mentions.len(), 3);
1733        assert!(matches!(mentions[0].1, MentionUri::File { .. }));
1734        assert!(matches!(mentions[1].1, MentionUri::Fetch { .. }));
1735        assert!(matches!(mentions[2].1, MentionUri::Directory { .. }));
1736
1737        // Adjacent mentions without separator
1738        let text = "[@a](file:///a)[@b](file:///b)";
1739        let mentions = parse_mention_links(text, PathStyle::local());
1740        assert_eq!(mentions.len(), 2);
1741
1742        // Regular markdown link (not a mention) should be ignored
1743        let text = "[regular link](https://example.com)";
1744        let mentions = parse_mention_links(text, PathStyle::local());
1745        assert_eq!(mentions.len(), 0);
1746
1747        // Incomplete mention link patterns
1748        let text = "[@name] without url and [@name( malformed";
1749        let mentions = parse_mention_links(text, PathStyle::local());
1750        assert_eq!(mentions.len(), 0);
1751
1752        // Nested brackets in name portion
1753        let text = "[@name [with brackets]](file:///path/to/file)";
1754        let mentions = parse_mention_links(text, PathStyle::local());
1755        assert_eq!(mentions.len(), 1);
1756        assert_eq!(mentions[0].0, 0..text.len());
1757
1758        // Deeply nested brackets
1759        let text = "[@outer [inner [deep]]](file:///path)";
1760        let mentions = parse_mention_links(text, PathStyle::local());
1761        assert_eq!(mentions.len(), 1);
1762
1763        // Unbalanced brackets should fail gracefully
1764        let text = "[@unbalanced [bracket](file:///path)";
1765        let mentions = parse_mention_links(text, PathStyle::local());
1766        assert_eq!(mentions.len(), 0);
1767
1768        // Nested parentheses in URI (common in URLs with query params)
1769        let text = "[@wiki](https://en.wikipedia.org/wiki/Rust_(programming_language))";
1770        let mentions = parse_mention_links(text, PathStyle::local());
1771        assert_eq!(mentions.len(), 1);
1772        if let MentionUri::Fetch { url } = &mentions[0].1 {
1773            assert!(url.as_str().contains("Rust_(programming_language)"));
1774        } else {
1775            panic!("Expected Fetch URI");
1776        }
1777    }
1778
1779    #[gpui::test]
1780    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1781        init_test(cx);
1782
1783        let fs = FakeFs::new(cx.executor());
1784        fs.insert_tree("/project", json!({"file": ""})).await;
1785        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1786
1787        let (multi_workspace, cx) =
1788            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1789        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1790
1791        let thread_store = None;
1792        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
1793
1794        let message_editor = cx.update(|window, cx| {
1795            cx.new(|cx| {
1796                MessageEditor::new(
1797                    workspace.downgrade(),
1798                    project.downgrade(),
1799                    thread_store.clone(),
1800                    history.downgrade(),
1801                    None,
1802                    Default::default(),
1803                    Default::default(),
1804                    "Test Agent".into(),
1805                    "Test",
1806                    EditorMode::AutoHeight {
1807                        min_lines: 1,
1808                        max_lines: None,
1809                    },
1810                    window,
1811                    cx,
1812                )
1813            })
1814        });
1815        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1816
1817        cx.run_until_parked();
1818
1819        let excerpt_id = editor.update(cx, |editor, cx| {
1820            editor
1821                .buffer()
1822                .read(cx)
1823                .excerpt_ids()
1824                .into_iter()
1825                .next()
1826                .unwrap()
1827        });
1828        let completions = editor.update_in(cx, |editor, window, cx| {
1829            editor.set_text("Hello @file ", window, cx);
1830            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1831            let completion_provider = editor.completion_provider().unwrap();
1832            completion_provider.completions(
1833                excerpt_id,
1834                &buffer,
1835                text::Anchor::MAX,
1836                CompletionContext {
1837                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1838                    trigger_character: Some("@".into()),
1839                },
1840                window,
1841                cx,
1842            )
1843        });
1844        let [_, completion]: [_; 2] = completions
1845            .await
1846            .unwrap()
1847            .into_iter()
1848            .flat_map(|response| response.completions)
1849            .collect::<Vec<_>>()
1850            .try_into()
1851            .unwrap();
1852
1853        editor.update_in(cx, |editor, window, cx| {
1854            let snapshot = editor.buffer().read(cx).snapshot(cx);
1855            let range = snapshot
1856                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1857                .unwrap();
1858            editor.edit([(range, completion.new_text)], cx);
1859            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1860        });
1861
1862        cx.run_until_parked();
1863
1864        // Backspace over the inserted crease (and the following space).
1865        editor.update_in(cx, |editor, window, cx| {
1866            editor.backspace(&Default::default(), window, cx);
1867            editor.backspace(&Default::default(), window, cx);
1868        });
1869
1870        let (content, _) = message_editor
1871            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1872            .await
1873            .unwrap();
1874
1875        // We don't send a resource link for the deleted crease.
1876        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1877    }
1878
1879    #[gpui::test]
1880    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1881        init_test(cx);
1882        let fs = FakeFs::new(cx.executor());
1883        fs.insert_tree(
1884            "/test",
1885            json!({
1886                ".zed": {
1887                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1888                },
1889                "src": {
1890                    "main.rs": "fn main() {}",
1891                },
1892            }),
1893        )
1894        .await;
1895
1896        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1897        let thread_store = None;
1898        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1899        // Start with no available commands - simulating Claude which doesn't support slash commands
1900        let available_commands = Rc::new(RefCell::new(vec![]));
1901
1902        let (multi_workspace, cx) =
1903            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1904        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1905        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
1906        let workspace_handle = workspace.downgrade();
1907        let message_editor = workspace.update_in(cx, |_, window, cx| {
1908            cx.new(|cx| {
1909                MessageEditor::new(
1910                    workspace_handle.clone(),
1911                    project.downgrade(),
1912                    thread_store.clone(),
1913                    history.downgrade(),
1914                    None,
1915                    prompt_capabilities.clone(),
1916                    available_commands.clone(),
1917                    "Claude Agent".into(),
1918                    "Test",
1919                    EditorMode::AutoHeight {
1920                        min_lines: 1,
1921                        max_lines: None,
1922                    },
1923                    window,
1924                    cx,
1925                )
1926            })
1927        });
1928        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1929
1930        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1931        editor.update_in(cx, |editor, window, cx| {
1932            editor.set_text("/file test.txt", window, cx);
1933        });
1934
1935        let contents_result = message_editor
1936            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1937            .await;
1938
1939        // Should fail because available_commands is empty (no commands supported)
1940        assert!(contents_result.is_err());
1941        let error_message = contents_result.unwrap_err().to_string();
1942        assert!(error_message.contains("not supported by Claude Agent"));
1943        assert!(error_message.contains("Available commands: none"));
1944
1945        // Now simulate Claude providing its list of available commands (which doesn't include file)
1946        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
1947
1948        // Test that unsupported slash commands trigger an error when we have a list of available commands
1949        editor.update_in(cx, |editor, window, cx| {
1950            editor.set_text("/file test.txt", window, cx);
1951        });
1952
1953        let contents_result = message_editor
1954            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1955            .await;
1956
1957        assert!(contents_result.is_err());
1958        let error_message = contents_result.unwrap_err().to_string();
1959        assert!(error_message.contains("not supported by Claude Agent"));
1960        assert!(error_message.contains("/file"));
1961        assert!(error_message.contains("Available commands: /help"));
1962
1963        // Test that supported commands work fine
1964        editor.update_in(cx, |editor, window, cx| {
1965            editor.set_text("/help", window, cx);
1966        });
1967
1968        let contents_result = message_editor
1969            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1970            .await;
1971
1972        // Should succeed because /help is in available_commands
1973        assert!(contents_result.is_ok());
1974
1975        // Test that regular text works fine
1976        editor.update_in(cx, |editor, window, cx| {
1977            editor.set_text("Hello Claude!", window, cx);
1978        });
1979
1980        let (content, _) = message_editor
1981            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1982            .await
1983            .unwrap();
1984
1985        assert_eq!(content.len(), 1);
1986        if let acp::ContentBlock::Text(text) = &content[0] {
1987            assert_eq!(text.text, "Hello Claude!");
1988        } else {
1989            panic!("Expected ContentBlock::Text");
1990        }
1991
1992        // Test that @ mentions still work
1993        editor.update_in(cx, |editor, window, cx| {
1994            editor.set_text("Check this @", window, cx);
1995        });
1996
1997        // The @ mention functionality should not be affected
1998        let (content, _) = message_editor
1999            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2000            .await
2001            .unwrap();
2002
2003        assert_eq!(content.len(), 1);
2004        if let acp::ContentBlock::Text(text) = &content[0] {
2005            assert_eq!(text.text, "Check this @");
2006        } else {
2007            panic!("Expected ContentBlock::Text");
2008        }
2009    }
2010
2011    struct MessageEditorItem(Entity<MessageEditor>);
2012
2013    impl Item for MessageEditorItem {
2014        type Event = ();
2015
2016        fn include_in_nav_history() -> bool {
2017            false
2018        }
2019
2020        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
2021            "Test".into()
2022        }
2023    }
2024
2025    impl EventEmitter<()> for MessageEditorItem {}
2026
2027    impl Focusable for MessageEditorItem {
2028        fn focus_handle(&self, cx: &App) -> FocusHandle {
2029            self.0.read(cx).focus_handle(cx)
2030        }
2031    }
2032
2033    impl Render for MessageEditorItem {
2034        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2035            self.0.clone().into_any_element()
2036        }
2037    }
2038
2039    #[gpui::test]
2040    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
2041        init_test(cx);
2042
2043        let app_state = cx.update(AppState::test);
2044
2045        cx.update(|cx| {
2046            editor::init(cx);
2047            workspace::init(app_state.clone(), cx);
2048        });
2049
2050        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2051        let window =
2052            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2053        let workspace = window
2054            .read_with(cx, |mw, _| mw.workspace().clone())
2055            .unwrap();
2056
2057        let mut cx = VisualTestContext::from_window(window.into(), cx);
2058
2059        let thread_store = None;
2060        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
2061        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
2062        let available_commands = Rc::new(RefCell::new(vec![
2063            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
2064            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
2065                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
2066                    "<name>",
2067                )),
2068            ),
2069        ]));
2070
2071        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2072            let workspace_handle = cx.weak_entity();
2073            let message_editor = cx.new(|cx| {
2074                MessageEditor::new(
2075                    workspace_handle,
2076                    project.downgrade(),
2077                    thread_store.clone(),
2078                    history.downgrade(),
2079                    None,
2080                    prompt_capabilities.clone(),
2081                    available_commands.clone(),
2082                    "Test Agent".into(),
2083                    "Test",
2084                    EditorMode::AutoHeight {
2085                        max_lines: None,
2086                        min_lines: 1,
2087                    },
2088                    window,
2089                    cx,
2090                )
2091            });
2092            workspace.active_pane().update(cx, |pane, cx| {
2093                pane.add_item(
2094                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2095                    true,
2096                    true,
2097                    None,
2098                    window,
2099                    cx,
2100                );
2101            });
2102            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2103            message_editor.read(cx).editor().clone()
2104        });
2105
2106        cx.simulate_input("/");
2107
2108        editor.update_in(&mut cx, |editor, window, cx| {
2109            assert_eq!(editor.text(cx), "/");
2110            assert!(editor.has_visible_completions_menu());
2111
2112            assert_eq!(
2113                current_completion_labels_with_documentation(editor),
2114                &[
2115                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
2116                    ("say-hello".into(), "Say hello to whoever you want".into())
2117                ]
2118            );
2119            editor.set_text("", window, cx);
2120        });
2121
2122        cx.simulate_input("/qui");
2123
2124        editor.update_in(&mut cx, |editor, window, cx| {
2125            assert_eq!(editor.text(cx), "/qui");
2126            assert!(editor.has_visible_completions_menu());
2127
2128            assert_eq!(
2129                current_completion_labels_with_documentation(editor),
2130                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
2131            );
2132            editor.set_text("", window, cx);
2133        });
2134
2135        editor.update_in(&mut cx, |editor, window, cx| {
2136            assert!(editor.has_visible_completions_menu());
2137            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2138        });
2139
2140        cx.run_until_parked();
2141
2142        editor.update_in(&mut cx, |editor, window, cx| {
2143            assert_eq!(editor.display_text(cx), "/quick-math ");
2144            assert!(!editor.has_visible_completions_menu());
2145            editor.set_text("", window, cx);
2146        });
2147
2148        cx.simulate_input("/say");
2149
2150        editor.update_in(&mut cx, |editor, _window, cx| {
2151            assert_eq!(editor.display_text(cx), "/say");
2152            assert!(editor.has_visible_completions_menu());
2153
2154            assert_eq!(
2155                current_completion_labels_with_documentation(editor),
2156                &[("say-hello".into(), "Say hello to whoever you want".into())]
2157            );
2158        });
2159
2160        editor.update_in(&mut cx, |editor, window, cx| {
2161            assert!(editor.has_visible_completions_menu());
2162            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2163        });
2164
2165        cx.run_until_parked();
2166
2167        editor.update_in(&mut cx, |editor, _window, cx| {
2168            assert_eq!(editor.text(cx), "/say-hello ");
2169            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2170            assert!(!editor.has_visible_completions_menu());
2171        });
2172
2173        cx.simulate_input("GPT5");
2174
2175        cx.run_until_parked();
2176
2177        editor.update_in(&mut cx, |editor, window, cx| {
2178            assert_eq!(editor.text(cx), "/say-hello GPT5");
2179            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2180            assert!(!editor.has_visible_completions_menu());
2181
2182            // Delete argument
2183            for _ in 0..5 {
2184                editor.backspace(&editor::actions::Backspace, window, cx);
2185            }
2186        });
2187
2188        cx.run_until_parked();
2189
2190        editor.update_in(&mut cx, |editor, window, cx| {
2191            assert_eq!(editor.text(cx), "/say-hello");
2192            // Hint is visible because argument was deleted
2193            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2194
2195            // Delete last command letter
2196            editor.backspace(&editor::actions::Backspace, window, cx);
2197        });
2198
2199        cx.run_until_parked();
2200
2201        editor.update_in(&mut cx, |editor, _window, cx| {
2202            // Hint goes away once command no longer matches an available one
2203            assert_eq!(editor.text(cx), "/say-hell");
2204            assert_eq!(editor.display_text(cx), "/say-hell");
2205            assert!(!editor.has_visible_completions_menu());
2206        });
2207    }
2208
2209    #[gpui::test]
2210    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2211        init_test(cx);
2212
2213        let app_state = cx.update(AppState::test);
2214
2215        cx.update(|cx| {
2216            editor::init(cx);
2217            workspace::init(app_state.clone(), cx);
2218        });
2219
2220        app_state
2221            .fs
2222            .as_fake()
2223            .insert_tree(
2224                path!("/dir"),
2225                json!({
2226                    "editor": "",
2227                    "a": {
2228                        "one.txt": "1",
2229                        "two.txt": "2",
2230                        "three.txt": "3",
2231                        "four.txt": "4"
2232                    },
2233                    "b": {
2234                        "five.txt": "5",
2235                        "six.txt": "6",
2236                        "seven.txt": "7",
2237                        "eight.txt": "8",
2238                    },
2239                    "x.png": "",
2240                }),
2241            )
2242            .await;
2243
2244        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2245        let window =
2246            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2247        let workspace = window
2248            .read_with(cx, |mw, _| mw.workspace().clone())
2249            .unwrap();
2250
2251        let worktree = project.update(cx, |project, cx| {
2252            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2253            assert_eq!(worktrees.len(), 1);
2254            worktrees.pop().unwrap()
2255        });
2256        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2257
2258        let mut cx = VisualTestContext::from_window(window.into(), cx);
2259
2260        let paths = vec![
2261            rel_path("a/one.txt"),
2262            rel_path("a/two.txt"),
2263            rel_path("a/three.txt"),
2264            rel_path("a/four.txt"),
2265            rel_path("b/five.txt"),
2266            rel_path("b/six.txt"),
2267            rel_path("b/seven.txt"),
2268            rel_path("b/eight.txt"),
2269        ];
2270
2271        let slash = PathStyle::local().primary_separator();
2272
2273        let mut opened_editors = Vec::new();
2274        for path in paths {
2275            let buffer = workspace
2276                .update_in(&mut cx, |workspace, window, cx| {
2277                    workspace.open_path(
2278                        ProjectPath {
2279                            worktree_id,
2280                            path: path.into(),
2281                        },
2282                        None,
2283                        false,
2284                        window,
2285                        cx,
2286                    )
2287                })
2288                .await
2289                .unwrap();
2290            opened_editors.push(buffer);
2291        }
2292
2293        let thread_store = cx.new(|cx| ThreadStore::new(cx));
2294        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
2295        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
2296
2297        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2298            let workspace_handle = cx.weak_entity();
2299            let message_editor = cx.new(|cx| {
2300                MessageEditor::new(
2301                    workspace_handle,
2302                    project.downgrade(),
2303                    Some(thread_store),
2304                    history.downgrade(),
2305                    None,
2306                    prompt_capabilities.clone(),
2307                    Default::default(),
2308                    "Test Agent".into(),
2309                    "Test",
2310                    EditorMode::AutoHeight {
2311                        max_lines: None,
2312                        min_lines: 1,
2313                    },
2314                    window,
2315                    cx,
2316                )
2317            });
2318            workspace.active_pane().update(cx, |pane, cx| {
2319                pane.add_item(
2320                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2321                    true,
2322                    true,
2323                    None,
2324                    window,
2325                    cx,
2326                );
2327            });
2328            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2329            let editor = message_editor.read(cx).editor().clone();
2330            (message_editor, editor)
2331        });
2332
2333        cx.simulate_input("Lorem @");
2334
2335        editor.update_in(&mut cx, |editor, window, cx| {
2336            assert_eq!(editor.text(cx), "Lorem @");
2337            assert!(editor.has_visible_completions_menu());
2338
2339            assert_eq!(
2340                current_completion_labels(editor),
2341                &[
2342                    format!("eight.txt b{slash}"),
2343                    format!("seven.txt b{slash}"),
2344                    format!("six.txt b{slash}"),
2345                    format!("five.txt b{slash}"),
2346                    "Files & Directories".into(),
2347                    "Symbols".into()
2348                ]
2349            );
2350            editor.set_text("", window, cx);
2351        });
2352
2353        prompt_capabilities.replace(
2354            acp::PromptCapabilities::new()
2355                .image(true)
2356                .audio(true)
2357                .embedded_context(true),
2358        );
2359
2360        cx.simulate_input("Lorem ");
2361
2362        editor.update(&mut cx, |editor, cx| {
2363            assert_eq!(editor.text(cx), "Lorem ");
2364            assert!(!editor.has_visible_completions_menu());
2365        });
2366
2367        cx.simulate_input("@");
2368
2369        editor.update(&mut cx, |editor, cx| {
2370            assert_eq!(editor.text(cx), "Lorem @");
2371            assert!(editor.has_visible_completions_menu());
2372            assert_eq!(
2373                current_completion_labels(editor),
2374                &[
2375                    format!("eight.txt b{slash}"),
2376                    format!("seven.txt b{slash}"),
2377                    format!("six.txt b{slash}"),
2378                    format!("five.txt b{slash}"),
2379                    "Files & Directories".into(),
2380                    "Symbols".into(),
2381                    "Threads".into(),
2382                    "Fetch".into()
2383                ]
2384            );
2385        });
2386
2387        // Select and confirm "File"
2388        editor.update_in(&mut cx, |editor, window, cx| {
2389            assert!(editor.has_visible_completions_menu());
2390            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2391            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2392            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2393            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2394            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2395        });
2396
2397        cx.run_until_parked();
2398
2399        editor.update(&mut cx, |editor, cx| {
2400            assert_eq!(editor.text(cx), "Lorem @file ");
2401            assert!(editor.has_visible_completions_menu());
2402        });
2403
2404        cx.simulate_input("one");
2405
2406        editor.update(&mut cx, |editor, cx| {
2407            assert_eq!(editor.text(cx), "Lorem @file one");
2408            assert!(editor.has_visible_completions_menu());
2409            assert_eq!(
2410                current_completion_labels(editor),
2411                vec![format!("one.txt a{slash}")]
2412            );
2413        });
2414
2415        editor.update_in(&mut cx, |editor, window, cx| {
2416            assert!(editor.has_visible_completions_menu());
2417            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2418        });
2419
2420        let url_one = MentionUri::File {
2421            abs_path: path!("/dir/a/one.txt").into(),
2422        }
2423        .to_uri()
2424        .to_string();
2425        editor.update(&mut cx, |editor, cx| {
2426            let text = editor.text(cx);
2427            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2428            assert!(!editor.has_visible_completions_menu());
2429            assert_eq!(fold_ranges(editor, cx).len(), 1);
2430        });
2431
2432        let contents = message_editor
2433            .update(&mut cx, |message_editor, cx| {
2434                message_editor
2435                    .mention_set()
2436                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2437            })
2438            .await
2439            .unwrap()
2440            .into_values()
2441            .collect::<Vec<_>>();
2442
2443        {
2444            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2445                panic!("Unexpected mentions");
2446            };
2447            pretty_assertions::assert_eq!(content, "1");
2448            pretty_assertions::assert_eq!(
2449                uri,
2450                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
2451            );
2452        }
2453
2454        cx.simulate_input(" ");
2455
2456        editor.update(&mut cx, |editor, cx| {
2457            let text = editor.text(cx);
2458            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2459            assert!(!editor.has_visible_completions_menu());
2460            assert_eq!(fold_ranges(editor, cx).len(), 1);
2461        });
2462
2463        cx.simulate_input("Ipsum ");
2464
2465        editor.update(&mut cx, |editor, cx| {
2466            let text = editor.text(cx);
2467            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2468            assert!(!editor.has_visible_completions_menu());
2469            assert_eq!(fold_ranges(editor, cx).len(), 1);
2470        });
2471
2472        cx.simulate_input("@file ");
2473
2474        editor.update(&mut cx, |editor, cx| {
2475            let text = editor.text(cx);
2476            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2477            assert!(editor.has_visible_completions_menu());
2478            assert_eq!(fold_ranges(editor, cx).len(), 1);
2479        });
2480
2481        editor.update_in(&mut cx, |editor, window, cx| {
2482            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2483        });
2484
2485        cx.run_until_parked();
2486
2487        let contents = message_editor
2488            .update(&mut cx, |message_editor, cx| {
2489                message_editor
2490                    .mention_set()
2491                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2492            })
2493            .await
2494            .unwrap()
2495            .into_values()
2496            .collect::<Vec<_>>();
2497
2498        let url_eight = MentionUri::File {
2499            abs_path: path!("/dir/b/eight.txt").into(),
2500        }
2501        .to_uri()
2502        .to_string();
2503
2504        {
2505            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2506                panic!("Unexpected mentions");
2507            };
2508            pretty_assertions::assert_eq!(content, "8");
2509            pretty_assertions::assert_eq!(
2510                uri,
2511                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
2512            );
2513        }
2514
2515        editor.update(&mut cx, |editor, cx| {
2516            assert_eq!(
2517                editor.text(cx),
2518                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2519            );
2520            assert!(!editor.has_visible_completions_menu());
2521            assert_eq!(fold_ranges(editor, cx).len(), 2);
2522        });
2523
2524        let plain_text_language = Arc::new(language::Language::new(
2525            language::LanguageConfig {
2526                name: "Plain Text".into(),
2527                matcher: language::LanguageMatcher {
2528                    path_suffixes: vec!["txt".to_string()],
2529                    ..Default::default()
2530                },
2531                ..Default::default()
2532            },
2533            None,
2534        ));
2535
2536        // Register the language and fake LSP
2537        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2538        language_registry.add(plain_text_language);
2539
2540        let mut fake_language_servers = language_registry.register_fake_lsp(
2541            "Plain Text",
2542            language::FakeLspAdapter {
2543                capabilities: lsp::ServerCapabilities {
2544                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2545                    ..Default::default()
2546                },
2547                ..Default::default()
2548            },
2549        );
2550
2551        // Open the buffer to trigger LSP initialization
2552        let buffer = project
2553            .update(&mut cx, |project, cx| {
2554                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2555            })
2556            .await
2557            .unwrap();
2558
2559        // Register the buffer with language servers
2560        let _handle = project.update(&mut cx, |project, cx| {
2561            project.register_buffer_with_language_servers(&buffer, cx)
2562        });
2563
2564        cx.run_until_parked();
2565
2566        let fake_language_server = fake_language_servers.next().await.unwrap();
2567        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2568            move |_, _| async move {
2569                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2570                    #[allow(deprecated)]
2571                    lsp::SymbolInformation {
2572                        name: "MySymbol".into(),
2573                        location: lsp::Location {
2574                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2575                            range: lsp::Range::new(
2576                                lsp::Position::new(0, 0),
2577                                lsp::Position::new(0, 1),
2578                            ),
2579                        },
2580                        kind: lsp::SymbolKind::CONSTANT,
2581                        tags: None,
2582                        container_name: None,
2583                        deprecated: None,
2584                    },
2585                ])))
2586            },
2587        );
2588
2589        cx.simulate_input("@symbol ");
2590
2591        editor.update(&mut cx, |editor, cx| {
2592            assert_eq!(
2593                editor.text(cx),
2594                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2595            );
2596            assert!(editor.has_visible_completions_menu());
2597            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
2598        });
2599
2600        editor.update_in(&mut cx, |editor, window, cx| {
2601            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2602        });
2603
2604        let symbol = MentionUri::Symbol {
2605            abs_path: path!("/dir/a/one.txt").into(),
2606            name: "MySymbol".into(),
2607            line_range: 0..=0,
2608        };
2609
2610        let contents = message_editor
2611            .update(&mut cx, |message_editor, cx| {
2612                message_editor
2613                    .mention_set()
2614                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2615            })
2616            .await
2617            .unwrap()
2618            .into_values()
2619            .collect::<Vec<_>>();
2620
2621        {
2622            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2623                panic!("Unexpected mentions");
2624            };
2625            pretty_assertions::assert_eq!(content, "1");
2626            pretty_assertions::assert_eq!(uri, &symbol);
2627        }
2628
2629        cx.run_until_parked();
2630
2631        editor.read_with(&cx, |editor, cx| {
2632            assert_eq!(
2633                editor.text(cx),
2634                format!(
2635                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2636                    symbol.to_uri(),
2637                )
2638            );
2639        });
2640
2641        // Try to mention an "image" file that will fail to load
2642        cx.simulate_input("@file x.png");
2643
2644        editor.update(&mut cx, |editor, cx| {
2645            assert_eq!(
2646                editor.text(cx),
2647                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2648            );
2649            assert!(editor.has_visible_completions_menu());
2650            assert_eq!(current_completion_labels(editor), &["x.png "]);
2651        });
2652
2653        editor.update_in(&mut cx, |editor, window, cx| {
2654            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2655        });
2656
2657        // Getting the message contents fails
2658        message_editor
2659            .update(&mut cx, |message_editor, cx| {
2660                message_editor
2661                    .mention_set()
2662                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2663            })
2664            .await
2665            .expect_err("Should fail to load x.png");
2666
2667        cx.run_until_parked();
2668
2669        // Mention was removed
2670        editor.read_with(&cx, |editor, cx| {
2671            assert_eq!(
2672                editor.text(cx),
2673                format!(
2674                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2675                    symbol.to_uri()
2676                )
2677            );
2678        });
2679
2680        // Once more
2681        cx.simulate_input("@file x.png");
2682
2683        editor.update(&mut cx, |editor, cx| {
2684                    assert_eq!(
2685                        editor.text(cx),
2686                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
2687                    );
2688                    assert!(editor.has_visible_completions_menu());
2689                    assert_eq!(current_completion_labels(editor), &["x.png "]);
2690                });
2691
2692        editor.update_in(&mut cx, |editor, window, cx| {
2693            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2694        });
2695
2696        // This time don't immediately get the contents, just let the confirmed completion settle
2697        cx.run_until_parked();
2698
2699        // Mention was removed
2700        editor.read_with(&cx, |editor, cx| {
2701            assert_eq!(
2702                editor.text(cx),
2703                format!(
2704                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
2705                    symbol.to_uri()
2706                )
2707            );
2708        });
2709
2710        // Now getting the contents succeeds, because the invalid mention was removed
2711        let contents = message_editor
2712            .update(&mut cx, |message_editor, cx| {
2713                message_editor
2714                    .mention_set()
2715                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2716            })
2717            .await
2718            .unwrap();
2719        assert_eq!(contents.len(), 3);
2720    }
2721
2722    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2723        let snapshot = editor.buffer().read(cx).snapshot(cx);
2724        editor.display_map.update(cx, |display_map, cx| {
2725            display_map
2726                .snapshot(cx)
2727                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
2728                .map(|fold| fold.range.to_point(&snapshot))
2729                .collect()
2730        })
2731    }
2732
2733    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2734        let completions = editor.current_completions().expect("Missing completions");
2735        completions
2736            .into_iter()
2737            .map(|completion| completion.label.text)
2738            .collect::<Vec<_>>()
2739    }
2740
2741    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2742        let completions = editor.current_completions().expect("Missing completions");
2743        completions
2744            .into_iter()
2745            .map(|completion| {
2746                (
2747                    completion.label.text,
2748                    completion
2749                        .documentation
2750                        .map(|d| d.text().to_string())
2751                        .unwrap_or_default(),
2752                )
2753            })
2754            .collect::<Vec<_>>()
2755    }
2756
2757    #[gpui::test]
2758    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
2759        init_test(cx);
2760
2761        let fs = FakeFs::new(cx.executor());
2762
2763        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2764        // Using plain text without a configured language, so no outline is available
2765        const LINE: &str = "This is a line of text in the file\n";
2766        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2767        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2768
2769        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2770        let small_content = "fn small_function() { /* small */ }\n";
2771        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2772
2773        fs.insert_tree(
2774            "/project",
2775            json!({
2776                "large_file.txt": large_content.clone(),
2777                "small_file.txt": small_content,
2778            }),
2779        )
2780        .await;
2781
2782        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2783
2784        let (multi_workspace, cx) =
2785            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2786        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2787
2788        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2789        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
2790
2791        let message_editor = cx.update(|window, cx| {
2792            cx.new(|cx| {
2793                let editor = MessageEditor::new(
2794                    workspace.downgrade(),
2795                    project.downgrade(),
2796                    thread_store.clone(),
2797                    history.downgrade(),
2798                    None,
2799                    Default::default(),
2800                    Default::default(),
2801                    "Test Agent".into(),
2802                    "Test",
2803                    EditorMode::AutoHeight {
2804                        min_lines: 1,
2805                        max_lines: None,
2806                    },
2807                    window,
2808                    cx,
2809                );
2810                // Enable embedded context so files are actually included
2811                editor
2812                    .prompt_capabilities
2813                    .replace(acp::PromptCapabilities::new().embedded_context(true));
2814                editor
2815            })
2816        });
2817
2818        // Test large file mention
2819        // Get the absolute path using the project's worktree
2820        let large_file_abs_path = project.read_with(cx, |project, cx| {
2821            let worktree = project.worktrees(cx).next().unwrap();
2822            let worktree_root = worktree.read(cx).abs_path();
2823            worktree_root.join("large_file.txt")
2824        });
2825        let large_file_task = message_editor.update(cx, |editor, cx| {
2826            editor.mention_set().update(cx, |set, cx| {
2827                set.confirm_mention_for_file(large_file_abs_path, true, cx)
2828            })
2829        });
2830
2831        let large_file_mention = large_file_task.await.unwrap();
2832        match large_file_mention {
2833            Mention::Text { content, .. } => {
2834                // Should contain some of the content but not all of it
2835                assert!(
2836                    content.contains(LINE),
2837                    "Should contain some of the file content"
2838                );
2839                assert!(
2840                    !content.contains(&LINE.repeat(100)),
2841                    "Should not contain the full file"
2842                );
2843                // Should be much smaller than original
2844                assert!(
2845                    content.len() < large_content.len() / 10,
2846                    "Should be significantly truncated"
2847                );
2848            }
2849            _ => panic!("Expected Text mention for large file"),
2850        }
2851
2852        // Test small file mention
2853        // Get the absolute path using the project's worktree
2854        let small_file_abs_path = project.read_with(cx, |project, cx| {
2855            let worktree = project.worktrees(cx).next().unwrap();
2856            let worktree_root = worktree.read(cx).abs_path();
2857            worktree_root.join("small_file.txt")
2858        });
2859        let small_file_task = message_editor.update(cx, |editor, cx| {
2860            editor.mention_set().update(cx, |set, cx| {
2861                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2862            })
2863        });
2864
2865        let small_file_mention = small_file_task.await.unwrap();
2866        match small_file_mention {
2867            Mention::Text { content, .. } => {
2868                // Should contain the full actual content
2869                assert_eq!(content, small_content);
2870            }
2871            _ => panic!("Expected Text mention for small file"),
2872        }
2873    }
2874
2875    #[gpui::test]
2876    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2877        init_test(cx);
2878        cx.update(LanguageModelRegistry::test);
2879
2880        let fs = FakeFs::new(cx.executor());
2881        fs.insert_tree("/project", json!({"file": ""})).await;
2882        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2883
2884        let (multi_workspace, cx) =
2885            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2886        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2887
2888        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
2889        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
2890
2891        let session_id = acp::SessionId::new("thread-123");
2892        let title = Some("Previous Conversation".into());
2893
2894        let message_editor = cx.update(|window, cx| {
2895            cx.new(|cx| {
2896                let mut editor = MessageEditor::new(
2897                    workspace.downgrade(),
2898                    project.downgrade(),
2899                    thread_store.clone(),
2900                    history.downgrade(),
2901                    None,
2902                    Default::default(),
2903                    Default::default(),
2904                    "Test Agent".into(),
2905                    "Test",
2906                    EditorMode::AutoHeight {
2907                        min_lines: 1,
2908                        max_lines: None,
2909                    },
2910                    window,
2911                    cx,
2912                );
2913                editor.insert_thread_summary(session_id.clone(), title.clone(), window, cx);
2914                editor
2915            })
2916        });
2917
2918        // Construct expected values for verification
2919        let expected_uri = MentionUri::Thread {
2920            id: session_id.clone(),
2921            name: title.as_ref().unwrap().to_string(),
2922        };
2923        let expected_title = title.as_ref().unwrap();
2924        let expected_link = format!("[@{}]({})", expected_title, expected_uri.to_uri());
2925
2926        message_editor.read_with(cx, |editor, cx| {
2927            let text = editor.text(cx);
2928
2929            assert!(
2930                text.contains(&expected_link),
2931                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2932                expected_link,
2933                text
2934            );
2935
2936            let mentions = editor.mention_set().read(cx).mentions();
2937            assert_eq!(
2938                mentions.len(),
2939                1,
2940                "Expected exactly one mention after inserting thread summary"
2941            );
2942
2943            assert!(
2944                mentions.contains(&expected_uri),
2945                "Expected mentions to contain the thread URI"
2946            );
2947        });
2948    }
2949
2950    #[gpui::test]
2951    async fn test_insert_thread_summary_skipped_for_external_agents(cx: &mut TestAppContext) {
2952        init_test(cx);
2953        cx.update(LanguageModelRegistry::test);
2954
2955        let fs = FakeFs::new(cx.executor());
2956        fs.insert_tree("/project", json!({"file": ""})).await;
2957        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2958
2959        let (multi_workspace, cx) =
2960            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2961        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2962
2963        let thread_store = None;
2964        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
2965
2966        let message_editor = cx.update(|window, cx| {
2967            cx.new(|cx| {
2968                let mut editor = MessageEditor::new(
2969                    workspace.downgrade(),
2970                    project.downgrade(),
2971                    thread_store.clone(),
2972                    history.downgrade(),
2973                    None,
2974                    Default::default(),
2975                    Default::default(),
2976                    "Test Agent".into(),
2977                    "Test",
2978                    EditorMode::AutoHeight {
2979                        min_lines: 1,
2980                        max_lines: None,
2981                    },
2982                    window,
2983                    cx,
2984                );
2985                editor.insert_thread_summary(
2986                    acp::SessionId::new("thread-123"),
2987                    Some("Previous Conversation".into()),
2988                    window,
2989                    cx,
2990                );
2991                editor
2992            })
2993        });
2994
2995        message_editor.read_with(cx, |editor, cx| {
2996            assert!(
2997                editor.text(cx).is_empty(),
2998                "Expected thread summary to be skipped for external agents"
2999            );
3000            assert!(
3001                editor.mention_set().read(cx).mentions().is_empty(),
3002                "Expected no mentions when thread summary is skipped"
3003            );
3004        });
3005    }
3006
3007    #[gpui::test]
3008    async fn test_thread_mode_hidden_when_disabled(cx: &mut TestAppContext) {
3009        init_test(cx);
3010
3011        let fs = FakeFs::new(cx.executor());
3012        fs.insert_tree("/project", json!({"file": ""})).await;
3013        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3014
3015        let (multi_workspace, cx) =
3016            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3017        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3018
3019        let thread_store = None;
3020        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3021
3022        let message_editor = cx.update(|window, cx| {
3023            cx.new(|cx| {
3024                MessageEditor::new(
3025                    workspace.downgrade(),
3026                    project.downgrade(),
3027                    thread_store.clone(),
3028                    history.downgrade(),
3029                    None,
3030                    Default::default(),
3031                    Default::default(),
3032                    "Test Agent".into(),
3033                    "Test",
3034                    EditorMode::AutoHeight {
3035                        min_lines: 1,
3036                        max_lines: None,
3037                    },
3038                    window,
3039                    cx,
3040                )
3041            })
3042        });
3043
3044        message_editor.update(cx, |editor, _cx| {
3045            editor
3046                .prompt_capabilities
3047                .replace(acp::PromptCapabilities::new().embedded_context(true));
3048        });
3049
3050        let supported_modes = {
3051            let app = cx.app.borrow();
3052            message_editor.supported_modes(&app)
3053        };
3054
3055        assert!(
3056            !supported_modes.contains(&PromptContextType::Thread),
3057            "Expected thread mode to be hidden when thread mentions are disabled"
3058        );
3059    }
3060
3061    #[gpui::test]
3062    async fn test_thread_mode_visible_when_enabled(cx: &mut TestAppContext) {
3063        init_test(cx);
3064
3065        let fs = FakeFs::new(cx.executor());
3066        fs.insert_tree("/project", json!({"file": ""})).await;
3067        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3068
3069        let (multi_workspace, cx) =
3070            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3071        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3072
3073        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3074        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3075
3076        let message_editor = cx.update(|window, cx| {
3077            cx.new(|cx| {
3078                MessageEditor::new(
3079                    workspace.downgrade(),
3080                    project.downgrade(),
3081                    thread_store.clone(),
3082                    history.downgrade(),
3083                    None,
3084                    Default::default(),
3085                    Default::default(),
3086                    "Test Agent".into(),
3087                    "Test",
3088                    EditorMode::AutoHeight {
3089                        min_lines: 1,
3090                        max_lines: None,
3091                    },
3092                    window,
3093                    cx,
3094                )
3095            })
3096        });
3097
3098        message_editor.update(cx, |editor, _cx| {
3099            editor
3100                .prompt_capabilities
3101                .replace(acp::PromptCapabilities::new().embedded_context(true));
3102        });
3103
3104        let supported_modes = {
3105            let app = cx.app.borrow();
3106            message_editor.supported_modes(&app)
3107        };
3108
3109        assert!(
3110            supported_modes.contains(&PromptContextType::Thread),
3111            "Expected thread mode to be visible when enabled"
3112        );
3113    }
3114
3115    #[gpui::test]
3116    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
3117        init_test(cx);
3118
3119        let fs = FakeFs::new(cx.executor());
3120        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
3121            .await;
3122        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3123
3124        let (multi_workspace, cx) =
3125            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3126        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3127
3128        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3129        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3130
3131        let message_editor = cx.update(|window, cx| {
3132            cx.new(|cx| {
3133                MessageEditor::new(
3134                    workspace.downgrade(),
3135                    project.downgrade(),
3136                    thread_store.clone(),
3137                    history.downgrade(),
3138                    None,
3139                    Default::default(),
3140                    Default::default(),
3141                    "Test Agent".into(),
3142                    "Test",
3143                    EditorMode::AutoHeight {
3144                        min_lines: 1,
3145                        max_lines: None,
3146                    },
3147                    window,
3148                    cx,
3149                )
3150            })
3151        });
3152        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
3153
3154        cx.run_until_parked();
3155
3156        editor.update_in(cx, |editor, window, cx| {
3157            editor.set_text("  \u{A0}してhello world  ", window, cx);
3158        });
3159
3160        let (content, _) = message_editor
3161            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
3162            .await
3163            .unwrap();
3164
3165        assert_eq!(content, vec!["してhello world".into()]);
3166    }
3167
3168    #[gpui::test]
3169    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
3170        init_test(cx);
3171
3172        let fs = FakeFs::new(cx.executor());
3173
3174        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
3175
3176        fs.insert_tree(
3177            "/project",
3178            json!({
3179                "src": {
3180                    "main.rs": file_content,
3181                }
3182            }),
3183        )
3184        .await;
3185
3186        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3187
3188        let (multi_workspace, cx) =
3189            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3190        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3191
3192        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3193        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3194
3195        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
3196            let workspace_handle = cx.weak_entity();
3197            let message_editor = cx.new(|cx| {
3198                MessageEditor::new(
3199                    workspace_handle,
3200                    project.downgrade(),
3201                    thread_store.clone(),
3202                    history.downgrade(),
3203                    None,
3204                    Default::default(),
3205                    Default::default(),
3206                    "Test Agent".into(),
3207                    "Test",
3208                    EditorMode::AutoHeight {
3209                        max_lines: None,
3210                        min_lines: 1,
3211                    },
3212                    window,
3213                    cx,
3214                )
3215            });
3216            workspace.active_pane().update(cx, |pane, cx| {
3217                pane.add_item(
3218                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3219                    true,
3220                    true,
3221                    None,
3222                    window,
3223                    cx,
3224                );
3225            });
3226            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3227            let editor = message_editor.read(cx).editor().clone();
3228            (message_editor, editor)
3229        });
3230
3231        cx.simulate_input("What is in @file main");
3232
3233        editor.update_in(cx, |editor, window, cx| {
3234            assert!(editor.has_visible_completions_menu());
3235            assert_eq!(editor.text(cx), "What is in @file main");
3236            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
3237        });
3238
3239        let content = message_editor
3240            .update(cx, |editor, cx| editor.contents(false, cx))
3241            .await
3242            .unwrap()
3243            .0;
3244
3245        let main_rs_uri = if cfg!(windows) {
3246            "file:///C:/project/src/main.rs"
3247        } else {
3248            "file:///project/src/main.rs"
3249        };
3250
3251        // When embedded context is `false` we should get a resource link
3252        pretty_assertions::assert_eq!(
3253            content,
3254            vec![
3255                "What is in ".into(),
3256                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
3257            ]
3258        );
3259
3260        message_editor.update(cx, |editor, _cx| {
3261            editor
3262                .prompt_capabilities
3263                .replace(acp::PromptCapabilities::new().embedded_context(true))
3264        });
3265
3266        let content = message_editor
3267            .update(cx, |editor, cx| editor.contents(false, cx))
3268            .await
3269            .unwrap()
3270            .0;
3271
3272        // When embedded context is `true` we should get a resource
3273        pretty_assertions::assert_eq!(
3274            content,
3275            vec![
3276                "What is in ".into(),
3277                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
3278                    acp::EmbeddedResourceResource::TextResourceContents(
3279                        acp::TextResourceContents::new(file_content, main_rs_uri)
3280                    )
3281                ))
3282            ]
3283        );
3284    }
3285
3286    #[gpui::test]
3287    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
3288        init_test(cx);
3289
3290        let app_state = cx.update(AppState::test);
3291
3292        cx.update(|cx| {
3293            editor::init(cx);
3294            workspace::init(app_state.clone(), cx);
3295        });
3296
3297        app_state
3298            .fs
3299            .as_fake()
3300            .insert_tree(
3301                path!("/dir"),
3302                json!({
3303                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
3304                }),
3305            )
3306            .await;
3307
3308        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3309        let window =
3310            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3311        let workspace = window
3312            .read_with(cx, |mw, _| mw.workspace().clone())
3313            .unwrap();
3314
3315        let worktree = project.update(cx, |project, cx| {
3316            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
3317            assert_eq!(worktrees.len(), 1);
3318            worktrees.pop().unwrap()
3319        });
3320        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
3321
3322        let mut cx = VisualTestContext::from_window(window.into(), cx);
3323
3324        // Open a regular editor with the created file, and select a portion of
3325        // the text that will be used for the selections that are meant to be
3326        // inserted in the agent panel.
3327        let editor = workspace
3328            .update_in(&mut cx, |workspace, window, cx| {
3329                workspace.open_path(
3330                    ProjectPath {
3331                        worktree_id,
3332                        path: rel_path("test.txt").into(),
3333                    },
3334                    None,
3335                    false,
3336                    window,
3337                    cx,
3338                )
3339            })
3340            .await
3341            .unwrap()
3342            .downcast::<Editor>()
3343            .unwrap();
3344
3345        editor.update_in(&mut cx, |editor, window, cx| {
3346            editor.change_selections(Default::default(), window, cx, |selections| {
3347                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
3348            });
3349        });
3350
3351        let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
3352        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3353
3354        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
3355        // to ensure we have a fixed viewport, so we can eventually actually
3356        // place the cursor outside of the visible area.
3357        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
3358            let workspace_handle = cx.weak_entity();
3359            let message_editor = cx.new(|cx| {
3360                MessageEditor::new(
3361                    workspace_handle,
3362                    project.downgrade(),
3363                    thread_store.clone(),
3364                    history.downgrade(),
3365                    None,
3366                    Default::default(),
3367                    Default::default(),
3368                    "Test Agent".into(),
3369                    "Test",
3370                    EditorMode::full(),
3371                    window,
3372                    cx,
3373                )
3374            });
3375            workspace.active_pane().update(cx, |pane, cx| {
3376                pane.add_item(
3377                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3378                    true,
3379                    true,
3380                    None,
3381                    window,
3382                    cx,
3383                );
3384            });
3385
3386            message_editor
3387        });
3388
3389        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3390            message_editor.editor.update(cx, |editor, cx| {
3391                // Update the Agent Panel's Message Editor text to have 100
3392                // lines, ensuring that the cursor is set at line 90 and that we
3393                // then scroll all the way to the top, so the cursor's position
3394                // remains off screen.
3395                let mut lines = String::new();
3396                for _ in 1..=100 {
3397                    lines.push_str(&"Another line in the agent panel's message editor\n");
3398                }
3399                editor.set_text(lines.as_str(), window, cx);
3400                editor.change_selections(Default::default(), window, cx, |selections| {
3401                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
3402                });
3403                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
3404            });
3405        });
3406
3407        cx.run_until_parked();
3408
3409        // Before proceeding, let's assert that the cursor is indeed off screen,
3410        // otherwise the rest of the test doesn't make sense.
3411        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3412            message_editor.editor.update(cx, |editor, cx| {
3413                let snapshot = editor.snapshot(window, cx);
3414                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3415                let scroll_top = snapshot.scroll_position().y as u32;
3416                let visible_lines = editor.visible_line_count().unwrap() as u32;
3417                let visible_range = scroll_top..(scroll_top + visible_lines);
3418
3419                assert!(!visible_range.contains(&cursor_row));
3420            })
3421        });
3422
3423        // Now let's insert the selection in the Agent Panel's editor and
3424        // confirm that, after the insertion, the cursor is now in the visible
3425        // range.
3426        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3427            message_editor.insert_selections(window, cx);
3428        });
3429
3430        cx.run_until_parked();
3431
3432        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3433            message_editor.editor.update(cx, |editor, cx| {
3434                let snapshot = editor.snapshot(window, cx);
3435                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
3436                let scroll_top = snapshot.scroll_position().y as u32;
3437                let visible_lines = editor.visible_line_count().unwrap() as u32;
3438                let visible_range = scroll_top..(scroll_top + visible_lines);
3439
3440                assert!(visible_range.contains(&cursor_row));
3441            })
3442        });
3443    }
3444
3445    #[gpui::test]
3446    async fn test_insert_context_with_multibyte_characters(cx: &mut TestAppContext) {
3447        init_test(cx);
3448
3449        let app_state = cx.update(AppState::test);
3450
3451        cx.update(|cx| {
3452            editor::init(cx);
3453            workspace::init(app_state.clone(), cx);
3454        });
3455
3456        app_state
3457            .fs
3458            .as_fake()
3459            .insert_tree(path!("/dir"), json!({}))
3460            .await;
3461
3462        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
3463        let window =
3464            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3465        let workspace = window
3466            .read_with(cx, |mw, _| mw.workspace().clone())
3467            .unwrap();
3468
3469        let mut cx = VisualTestContext::from_window(window.into(), cx);
3470
3471        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3472        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3473
3474        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3475            let workspace_handle = cx.weak_entity();
3476            let message_editor = cx.new(|cx| {
3477                MessageEditor::new(
3478                    workspace_handle,
3479                    project.downgrade(),
3480                    Some(thread_store),
3481                    history.downgrade(),
3482                    None,
3483                    Default::default(),
3484                    Default::default(),
3485                    "Test Agent".into(),
3486                    "Test",
3487                    EditorMode::AutoHeight {
3488                        max_lines: None,
3489                        min_lines: 1,
3490                    },
3491                    window,
3492                    cx,
3493                )
3494            });
3495            workspace.active_pane().update(cx, |pane, cx| {
3496                pane.add_item(
3497                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3498                    true,
3499                    true,
3500                    None,
3501                    window,
3502                    cx,
3503                );
3504            });
3505            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3506            let editor = message_editor.read(cx).editor().clone();
3507            (message_editor, editor)
3508        });
3509
3510        editor.update_in(&mut cx, |editor, window, cx| {
3511            editor.set_text("😄😄", window, cx);
3512        });
3513
3514        cx.run_until_parked();
3515
3516        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3517            message_editor.insert_context_type("file", window, cx);
3518        });
3519
3520        cx.run_until_parked();
3521
3522        editor.update(&mut cx, |editor, cx| {
3523            assert_eq!(editor.text(cx), "😄😄@file");
3524        });
3525    }
3526
3527    #[gpui::test]
3528    async fn test_paste_mention_link_with_multiple_selections(cx: &mut TestAppContext) {
3529        init_test(cx);
3530
3531        let app_state = cx.update(AppState::test);
3532
3533        cx.update(|cx| {
3534            editor::init(cx);
3535            workspace::init(app_state.clone(), cx);
3536        });
3537
3538        app_state
3539            .fs
3540            .as_fake()
3541            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
3542            .await;
3543
3544        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
3545        let window =
3546            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3547        let workspace = window
3548            .read_with(cx, |mw, _| mw.workspace().clone())
3549            .unwrap();
3550
3551        let mut cx = VisualTestContext::from_window(window.into(), cx);
3552
3553        let thread_store = cx.new(|cx| ThreadStore::new(cx));
3554        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3555
3556        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
3557            let workspace_handle = cx.weak_entity();
3558            let message_editor = cx.new(|cx| {
3559                MessageEditor::new(
3560                    workspace_handle,
3561                    project.downgrade(),
3562                    Some(thread_store),
3563                    history.downgrade(),
3564                    None,
3565                    Default::default(),
3566                    Default::default(),
3567                    "Test Agent".into(),
3568                    "Test",
3569                    EditorMode::AutoHeight {
3570                        max_lines: None,
3571                        min_lines: 1,
3572                    },
3573                    window,
3574                    cx,
3575                )
3576            });
3577            workspace.active_pane().update(cx, |pane, cx| {
3578                pane.add_item(
3579                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
3580                    true,
3581                    true,
3582                    None,
3583                    window,
3584                    cx,
3585                );
3586            });
3587            message_editor.read(cx).focus_handle(cx).focus(window, cx);
3588            let editor = message_editor.read(cx).editor().clone();
3589            (message_editor, editor)
3590        });
3591
3592        editor.update_in(&mut cx, |editor, window, cx| {
3593            editor.set_text(
3594                "AAAAAAAAAAAAAAAAAAAAAAAAA     AAAAAAAAAAAAAAAAAAAAAAAAA",
3595                window,
3596                cx,
3597            );
3598        });
3599
3600        cx.run_until_parked();
3601
3602        editor.update_in(&mut cx, |editor, window, cx| {
3603            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3604                s.select_ranges([
3605                    MultiBufferOffset(0)..MultiBufferOffset(25), // First selection (large)
3606                    MultiBufferOffset(30)..MultiBufferOffset(55), // Second selection (newest)
3607                ]);
3608            });
3609        });
3610
3611        let mention_link = "[@f](file:///test.txt)";
3612        cx.write_to_clipboard(ClipboardItem::new_string(mention_link.into()));
3613
3614        message_editor.update_in(&mut cx, |message_editor, window, cx| {
3615            message_editor.paste(&Paste, window, cx);
3616        });
3617
3618        let text = editor.update(&mut cx, |editor, cx| editor.text(cx));
3619        assert!(
3620            text.contains("[@f](file:///test.txt)"),
3621            "Expected mention link to be pasted, got: {}",
3622            text
3623        );
3624    }
3625
3626    // Helper that creates a minimal MessageEditor inside a window, returning both
3627    // the entity and the underlying VisualTestContext so callers can drive updates.
3628    async fn setup_message_editor(
3629        cx: &mut TestAppContext,
3630    ) -> (Entity<MessageEditor>, &mut VisualTestContext) {
3631        let fs = FakeFs::new(cx.executor());
3632        fs.insert_tree("/project", json!({"file.txt": ""})).await;
3633        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3634
3635        let (multi_workspace, cx) =
3636            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3637        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3638        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3639
3640        let message_editor = cx.update(|window, cx| {
3641            cx.new(|cx| {
3642                MessageEditor::new(
3643                    workspace.downgrade(),
3644                    project.downgrade(),
3645                    None,
3646                    history.downgrade(),
3647                    None,
3648                    Default::default(),
3649                    Default::default(),
3650                    "Test Agent".into(),
3651                    "Test",
3652                    EditorMode::AutoHeight {
3653                        min_lines: 1,
3654                        max_lines: None,
3655                    },
3656                    window,
3657                    cx,
3658                )
3659            })
3660        });
3661
3662        cx.run_until_parked();
3663        (message_editor, cx)
3664    }
3665
3666    #[gpui::test]
3667    async fn test_set_message_plain_text(cx: &mut TestAppContext) {
3668        init_test(cx);
3669        let (message_editor, cx) = setup_message_editor(cx).await;
3670
3671        message_editor.update_in(cx, |editor, window, cx| {
3672            editor.set_message(
3673                vec![acp::ContentBlock::Text(acp::TextContent::new(
3674                    "hello world".to_string(),
3675                ))],
3676                window,
3677                cx,
3678            );
3679        });
3680
3681        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3682        assert_eq!(text, "hello world");
3683        assert!(!message_editor.update(cx, |editor, cx| editor.is_empty(cx)));
3684    }
3685
3686    #[gpui::test]
3687    async fn test_set_message_replaces_existing_content(cx: &mut TestAppContext) {
3688        init_test(cx);
3689        let (message_editor, cx) = setup_message_editor(cx).await;
3690
3691        // Set initial content.
3692        message_editor.update_in(cx, |editor, window, cx| {
3693            editor.set_message(
3694                vec![acp::ContentBlock::Text(acp::TextContent::new(
3695                    "old content".to_string(),
3696                ))],
3697                window,
3698                cx,
3699            );
3700        });
3701
3702        // Replace with new content.
3703        message_editor.update_in(cx, |editor, window, cx| {
3704            editor.set_message(
3705                vec![acp::ContentBlock::Text(acp::TextContent::new(
3706                    "new content".to_string(),
3707                ))],
3708                window,
3709                cx,
3710            );
3711        });
3712
3713        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3714        assert_eq!(
3715            text, "new content",
3716            "set_message should replace old content"
3717        );
3718    }
3719
3720    #[gpui::test]
3721    async fn test_append_message_to_empty_editor(cx: &mut TestAppContext) {
3722        init_test(cx);
3723        let (message_editor, cx) = setup_message_editor(cx).await;
3724
3725        message_editor.update_in(cx, |editor, window, cx| {
3726            editor.append_message(
3727                vec![acp::ContentBlock::Text(acp::TextContent::new(
3728                    "appended".to_string(),
3729                ))],
3730                Some("\n\n"),
3731                window,
3732                cx,
3733            );
3734        });
3735
3736        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3737        assert_eq!(
3738            text, "appended",
3739            "No separator should be inserted when the editor is empty"
3740        );
3741    }
3742
3743    #[gpui::test]
3744    async fn test_append_message_to_non_empty_editor(cx: &mut TestAppContext) {
3745        init_test(cx);
3746        let (message_editor, cx) = setup_message_editor(cx).await;
3747
3748        // Seed initial content.
3749        message_editor.update_in(cx, |editor, window, cx| {
3750            editor.set_message(
3751                vec![acp::ContentBlock::Text(acp::TextContent::new(
3752                    "initial".to_string(),
3753                ))],
3754                window,
3755                cx,
3756            );
3757        });
3758
3759        // Append with separator.
3760        message_editor.update_in(cx, |editor, window, cx| {
3761            editor.append_message(
3762                vec![acp::ContentBlock::Text(acp::TextContent::new(
3763                    "appended".to_string(),
3764                ))],
3765                Some("\n\n"),
3766                window,
3767                cx,
3768            );
3769        });
3770
3771        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3772        assert_eq!(
3773            text, "initial\n\nappended",
3774            "Separator should appear between existing and appended content"
3775        );
3776    }
3777
3778    #[gpui::test]
3779    async fn test_append_message_preserves_mention_offset(cx: &mut TestAppContext) {
3780        init_test(cx);
3781
3782        let fs = FakeFs::new(cx.executor());
3783        fs.insert_tree("/project", json!({"file.txt": "content"}))
3784            .await;
3785        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
3786
3787        let (multi_workspace, cx) =
3788            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3789        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3790        let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
3791
3792        let message_editor = cx.update(|window, cx| {
3793            cx.new(|cx| {
3794                MessageEditor::new(
3795                    workspace.downgrade(),
3796                    project.downgrade(),
3797                    None,
3798                    history.downgrade(),
3799                    None,
3800                    Default::default(),
3801                    Default::default(),
3802                    "Test Agent".into(),
3803                    "Test",
3804                    EditorMode::AutoHeight {
3805                        min_lines: 1,
3806                        max_lines: None,
3807                    },
3808                    window,
3809                    cx,
3810                )
3811            })
3812        });
3813
3814        cx.run_until_parked();
3815
3816        // Seed plain-text prefix so the editor is non-empty before appending.
3817        message_editor.update_in(cx, |editor, window, cx| {
3818            editor.set_message(
3819                vec![acp::ContentBlock::Text(acp::TextContent::new(
3820                    "prefix text".to_string(),
3821                ))],
3822                window,
3823                cx,
3824            );
3825        });
3826
3827        // Append a message that contains a ResourceLink mention.
3828        message_editor.update_in(cx, |editor, window, cx| {
3829            editor.append_message(
3830                vec![acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
3831                    "file.txt",
3832                    "file:///project/file.txt",
3833                ))],
3834                Some("\n\n"),
3835                window,
3836                cx,
3837            );
3838        });
3839
3840        cx.run_until_parked();
3841
3842        // The mention should be registered in the mention_set so that contents()
3843        // will emit it as a structured block rather than plain text.
3844        let mention_uris =
3845            message_editor.update(cx, |editor, cx| editor.mention_set.read(cx).mentions());
3846        assert_eq!(
3847            mention_uris.len(),
3848            1,
3849            "Expected exactly one mention in the mention_set after append, got: {mention_uris:?}"
3850        );
3851
3852        // The editor text should start with the prefix, then the separator, then
3853        // the mention placeholder — confirming the offset was computed correctly.
3854        let text = message_editor.update(cx, |editor, cx| editor.text(cx));
3855        assert!(
3856            text.starts_with("prefix text\n\n"),
3857            "Expected text to start with 'prefix text\\n\\n', got: {text:?}"
3858        );
3859    }
3860}