message_editor.rs

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