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