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