message_editor.rs

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