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::ThreadStore;
  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<ThreadStore>,
 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::{ThreadStore, outline};
1066    use agent_client_protocol as acp;
1067    use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset};
1068    use fs::FakeFs;
1069    use futures::StreamExt as _;
1070    use gpui::{
1071        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1072    };
1073    use language_model::LanguageModelRegistry;
1074    use lsp::{CompletionContext, CompletionTriggerKind};
1075    use project::{CompletionIntent, Project, ProjectPath};
1076    use serde_json::json;
1077    use text::Point;
1078    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1079    use util::{path, paths::PathStyle, rel_path::rel_path};
1080    use workspace::{AppState, Item, Workspace};
1081
1082    use crate::acp::{
1083        message_editor::{Mention, MessageEditor},
1084        thread_view::tests::init_test,
1085    };
1086
1087    #[gpui::test]
1088    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1089        init_test(cx);
1090
1091        let fs = FakeFs::new(cx.executor());
1092        fs.insert_tree("/project", json!({"file": ""})).await;
1093        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1094
1095        let (workspace, cx) =
1096            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1097
1098        let history_store = cx.new(|cx| ThreadStore::new(cx));
1099
1100        let message_editor = cx.update(|window, cx| {
1101            cx.new(|cx| {
1102                MessageEditor::new(
1103                    workspace.downgrade(),
1104                    project.downgrade(),
1105                    history_store.clone(),
1106                    None,
1107                    Default::default(),
1108                    Default::default(),
1109                    "Test Agent".into(),
1110                    "Test",
1111                    EditorMode::AutoHeight {
1112                        min_lines: 1,
1113                        max_lines: None,
1114                    },
1115                    window,
1116                    cx,
1117                )
1118            })
1119        });
1120        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1121
1122        cx.run_until_parked();
1123
1124        let excerpt_id = editor.update(cx, |editor, cx| {
1125            editor
1126                .buffer()
1127                .read(cx)
1128                .excerpt_ids()
1129                .into_iter()
1130                .next()
1131                .unwrap()
1132        });
1133        let completions = editor.update_in(cx, |editor, window, cx| {
1134            editor.set_text("Hello @file ", window, cx);
1135            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1136            let completion_provider = editor.completion_provider().unwrap();
1137            completion_provider.completions(
1138                excerpt_id,
1139                &buffer,
1140                text::Anchor::MAX,
1141                CompletionContext {
1142                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1143                    trigger_character: Some("@".into()),
1144                },
1145                window,
1146                cx,
1147            )
1148        });
1149        let [_, completion]: [_; 2] = completions
1150            .await
1151            .unwrap()
1152            .into_iter()
1153            .flat_map(|response| response.completions)
1154            .collect::<Vec<_>>()
1155            .try_into()
1156            .unwrap();
1157
1158        editor.update_in(cx, |editor, window, cx| {
1159            let snapshot = editor.buffer().read(cx).snapshot(cx);
1160            let range = snapshot
1161                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1162                .unwrap();
1163            editor.edit([(range, completion.new_text)], cx);
1164            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1165        });
1166
1167        cx.run_until_parked();
1168
1169        // Backspace over the inserted crease (and the following space).
1170        editor.update_in(cx, |editor, window, cx| {
1171            editor.backspace(&Default::default(), window, cx);
1172            editor.backspace(&Default::default(), window, cx);
1173        });
1174
1175        let (content, _) = message_editor
1176            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1177            .await
1178            .unwrap();
1179
1180        // We don't send a resource link for the deleted crease.
1181        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1182    }
1183
1184    #[gpui::test]
1185    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1186        init_test(cx);
1187        let fs = FakeFs::new(cx.executor());
1188        fs.insert_tree(
1189            "/test",
1190            json!({
1191                ".zed": {
1192                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1193                },
1194                "src": {
1195                    "main.rs": "fn main() {}",
1196                },
1197            }),
1198        )
1199        .await;
1200
1201        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1202        let history_store = cx.new(|cx| ThreadStore::new(cx));
1203        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1204        // Start with no available commands - simulating Claude which doesn't support slash commands
1205        let available_commands = Rc::new(RefCell::new(vec![]));
1206
1207        let (workspace, cx) =
1208            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1209        let workspace_handle = workspace.downgrade();
1210        let message_editor = workspace.update_in(cx, |_, window, cx| {
1211            cx.new(|cx| {
1212                MessageEditor::new(
1213                    workspace_handle.clone(),
1214                    project.downgrade(),
1215                    history_store.clone(),
1216                    None,
1217                    prompt_capabilities.clone(),
1218                    available_commands.clone(),
1219                    "Claude Code".into(),
1220                    "Test",
1221                    EditorMode::AutoHeight {
1222                        min_lines: 1,
1223                        max_lines: None,
1224                    },
1225                    window,
1226                    cx,
1227                )
1228            })
1229        });
1230        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1231
1232        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1233        editor.update_in(cx, |editor, window, cx| {
1234            editor.set_text("/file test.txt", window, cx);
1235        });
1236
1237        let contents_result = message_editor
1238            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1239            .await;
1240
1241        // Should fail because available_commands is empty (no commands supported)
1242        assert!(contents_result.is_err());
1243        let error_message = contents_result.unwrap_err().to_string();
1244        assert!(error_message.contains("not supported by Claude Code"));
1245        assert!(error_message.contains("Available commands: none"));
1246
1247        // Now simulate Claude providing its list of available commands (which doesn't include file)
1248        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
1249
1250        // Test that unsupported slash commands trigger an error when we have a list of available commands
1251        editor.update_in(cx, |editor, window, cx| {
1252            editor.set_text("/file test.txt", window, cx);
1253        });
1254
1255        let contents_result = message_editor
1256            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1257            .await;
1258
1259        assert!(contents_result.is_err());
1260        let error_message = contents_result.unwrap_err().to_string();
1261        assert!(error_message.contains("not supported by Claude Code"));
1262        assert!(error_message.contains("/file"));
1263        assert!(error_message.contains("Available commands: /help"));
1264
1265        // Test that supported commands work fine
1266        editor.update_in(cx, |editor, window, cx| {
1267            editor.set_text("/help", window, cx);
1268        });
1269
1270        let contents_result = message_editor
1271            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1272            .await;
1273
1274        // Should succeed because /help is in available_commands
1275        assert!(contents_result.is_ok());
1276
1277        // Test that regular text works fine
1278        editor.update_in(cx, |editor, window, cx| {
1279            editor.set_text("Hello Claude!", window, cx);
1280        });
1281
1282        let (content, _) = message_editor
1283            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1284            .await
1285            .unwrap();
1286
1287        assert_eq!(content.len(), 1);
1288        if let acp::ContentBlock::Text(text) = &content[0] {
1289            assert_eq!(text.text, "Hello Claude!");
1290        } else {
1291            panic!("Expected ContentBlock::Text");
1292        }
1293
1294        // Test that @ mentions still work
1295        editor.update_in(cx, |editor, window, cx| {
1296            editor.set_text("Check this @", window, cx);
1297        });
1298
1299        // The @ mention functionality should not be affected
1300        let (content, _) = message_editor
1301            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1302            .await
1303            .unwrap();
1304
1305        assert_eq!(content.len(), 1);
1306        if let acp::ContentBlock::Text(text) = &content[0] {
1307            assert_eq!(text.text, "Check this @");
1308        } else {
1309            panic!("Expected ContentBlock::Text");
1310        }
1311    }
1312
1313    struct MessageEditorItem(Entity<MessageEditor>);
1314
1315    impl Item for MessageEditorItem {
1316        type Event = ();
1317
1318        fn include_in_nav_history() -> bool {
1319            false
1320        }
1321
1322        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1323            "Test".into()
1324        }
1325    }
1326
1327    impl EventEmitter<()> for MessageEditorItem {}
1328
1329    impl Focusable for MessageEditorItem {
1330        fn focus_handle(&self, cx: &App) -> FocusHandle {
1331            self.0.read(cx).focus_handle(cx)
1332        }
1333    }
1334
1335    impl Render for MessageEditorItem {
1336        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1337            self.0.clone().into_any_element()
1338        }
1339    }
1340
1341    #[gpui::test]
1342    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1343        init_test(cx);
1344
1345        let app_state = cx.update(AppState::test);
1346
1347        cx.update(|cx| {
1348            editor::init(cx);
1349            workspace::init(app_state.clone(), cx);
1350        });
1351
1352        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1353        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1354        let workspace = window.root(cx).unwrap();
1355
1356        let mut cx = VisualTestContext::from_window(*window, cx);
1357
1358        let history_store = cx.new(|cx| ThreadStore::new(cx));
1359        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1360        let available_commands = Rc::new(RefCell::new(vec![
1361            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
1362            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
1363                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
1364                    "<name>",
1365                )),
1366            ),
1367        ]));
1368
1369        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1370            let workspace_handle = cx.weak_entity();
1371            let message_editor = cx.new(|cx| {
1372                MessageEditor::new(
1373                    workspace_handle,
1374                    project.downgrade(),
1375                    history_store.clone(),
1376                    None,
1377                    prompt_capabilities.clone(),
1378                    available_commands.clone(),
1379                    "Test Agent".into(),
1380                    "Test",
1381                    EditorMode::AutoHeight {
1382                        max_lines: None,
1383                        min_lines: 1,
1384                    },
1385                    window,
1386                    cx,
1387                )
1388            });
1389            workspace.active_pane().update(cx, |pane, cx| {
1390                pane.add_item(
1391                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1392                    true,
1393                    true,
1394                    None,
1395                    window,
1396                    cx,
1397                );
1398            });
1399            message_editor.read(cx).focus_handle(cx).focus(window, cx);
1400            message_editor.read(cx).editor().clone()
1401        });
1402
1403        cx.simulate_input("/");
1404
1405        editor.update_in(&mut cx, |editor, window, cx| {
1406            assert_eq!(editor.text(cx), "/");
1407            assert!(editor.has_visible_completions_menu());
1408
1409            assert_eq!(
1410                current_completion_labels_with_documentation(editor),
1411                &[
1412                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1413                    ("say-hello".into(), "Say hello to whoever you want".into())
1414                ]
1415            );
1416            editor.set_text("", window, cx);
1417        });
1418
1419        cx.simulate_input("/qui");
1420
1421        editor.update_in(&mut cx, |editor, window, cx| {
1422            assert_eq!(editor.text(cx), "/qui");
1423            assert!(editor.has_visible_completions_menu());
1424
1425            assert_eq!(
1426                current_completion_labels_with_documentation(editor),
1427                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1428            );
1429            editor.set_text("", window, cx);
1430        });
1431
1432        editor.update_in(&mut cx, |editor, window, cx| {
1433            assert!(editor.has_visible_completions_menu());
1434            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1435        });
1436
1437        cx.run_until_parked();
1438
1439        editor.update_in(&mut cx, |editor, window, cx| {
1440            assert_eq!(editor.display_text(cx), "/quick-math ");
1441            assert!(!editor.has_visible_completions_menu());
1442            editor.set_text("", window, cx);
1443        });
1444
1445        cx.simulate_input("/say");
1446
1447        editor.update_in(&mut cx, |editor, _window, cx| {
1448            assert_eq!(editor.display_text(cx), "/say");
1449            assert!(editor.has_visible_completions_menu());
1450
1451            assert_eq!(
1452                current_completion_labels_with_documentation(editor),
1453                &[("say-hello".into(), "Say hello to whoever you want".into())]
1454            );
1455        });
1456
1457        editor.update_in(&mut cx, |editor, window, cx| {
1458            assert!(editor.has_visible_completions_menu());
1459            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1460        });
1461
1462        cx.run_until_parked();
1463
1464        editor.update_in(&mut cx, |editor, _window, cx| {
1465            assert_eq!(editor.text(cx), "/say-hello ");
1466            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1467            assert!(!editor.has_visible_completions_menu());
1468        });
1469
1470        cx.simulate_input("GPT5");
1471
1472        cx.run_until_parked();
1473
1474        editor.update_in(&mut cx, |editor, window, cx| {
1475            assert_eq!(editor.text(cx), "/say-hello GPT5");
1476            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
1477            assert!(!editor.has_visible_completions_menu());
1478
1479            // Delete argument
1480            for _ in 0..5 {
1481                editor.backspace(&editor::actions::Backspace, window, cx);
1482            }
1483        });
1484
1485        cx.run_until_parked();
1486
1487        editor.update_in(&mut cx, |editor, window, cx| {
1488            assert_eq!(editor.text(cx), "/say-hello");
1489            // Hint is visible because argument was deleted
1490            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1491
1492            // Delete last command letter
1493            editor.backspace(&editor::actions::Backspace, window, cx);
1494        });
1495
1496        cx.run_until_parked();
1497
1498        editor.update_in(&mut cx, |editor, _window, cx| {
1499            // Hint goes away once command no longer matches an available one
1500            assert_eq!(editor.text(cx), "/say-hell");
1501            assert_eq!(editor.display_text(cx), "/say-hell");
1502            assert!(!editor.has_visible_completions_menu());
1503        });
1504    }
1505
1506    #[gpui::test]
1507    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
1508        init_test(cx);
1509
1510        let app_state = cx.update(AppState::test);
1511
1512        cx.update(|cx| {
1513            editor::init(cx);
1514            workspace::init(app_state.clone(), cx);
1515        });
1516
1517        app_state
1518            .fs
1519            .as_fake()
1520            .insert_tree(
1521                path!("/dir"),
1522                json!({
1523                    "editor": "",
1524                    "a": {
1525                        "one.txt": "1",
1526                        "two.txt": "2",
1527                        "three.txt": "3",
1528                        "four.txt": "4"
1529                    },
1530                    "b": {
1531                        "five.txt": "5",
1532                        "six.txt": "6",
1533                        "seven.txt": "7",
1534                        "eight.txt": "8",
1535                    },
1536                    "x.png": "",
1537                }),
1538            )
1539            .await;
1540
1541        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1542        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1543        let workspace = window.root(cx).unwrap();
1544
1545        let worktree = project.update(cx, |project, cx| {
1546            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1547            assert_eq!(worktrees.len(), 1);
1548            worktrees.pop().unwrap()
1549        });
1550        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1551
1552        let mut cx = VisualTestContext::from_window(*window, cx);
1553
1554        let paths = vec![
1555            rel_path("a/one.txt"),
1556            rel_path("a/two.txt"),
1557            rel_path("a/three.txt"),
1558            rel_path("a/four.txt"),
1559            rel_path("b/five.txt"),
1560            rel_path("b/six.txt"),
1561            rel_path("b/seven.txt"),
1562            rel_path("b/eight.txt"),
1563        ];
1564
1565        let slash = PathStyle::local().primary_separator();
1566
1567        let mut opened_editors = Vec::new();
1568        for path in paths {
1569            let buffer = workspace
1570                .update_in(&mut cx, |workspace, window, cx| {
1571                    workspace.open_path(
1572                        ProjectPath {
1573                            worktree_id,
1574                            path: path.into(),
1575                        },
1576                        None,
1577                        false,
1578                        window,
1579                        cx,
1580                    )
1581                })
1582                .await
1583                .unwrap();
1584            opened_editors.push(buffer);
1585        }
1586
1587        let history_store = cx.new(|cx| ThreadStore::new(cx));
1588        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1589
1590        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1591            let workspace_handle = cx.weak_entity();
1592            let message_editor = cx.new(|cx| {
1593                MessageEditor::new(
1594                    workspace_handle,
1595                    project.downgrade(),
1596                    history_store.clone(),
1597                    None,
1598                    prompt_capabilities.clone(),
1599                    Default::default(),
1600                    "Test Agent".into(),
1601                    "Test",
1602                    EditorMode::AutoHeight {
1603                        max_lines: None,
1604                        min_lines: 1,
1605                    },
1606                    window,
1607                    cx,
1608                )
1609            });
1610            workspace.active_pane().update(cx, |pane, cx| {
1611                pane.add_item(
1612                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1613                    true,
1614                    true,
1615                    None,
1616                    window,
1617                    cx,
1618                );
1619            });
1620            message_editor.read(cx).focus_handle(cx).focus(window, cx);
1621            let editor = message_editor.read(cx).editor().clone();
1622            (message_editor, editor)
1623        });
1624
1625        cx.simulate_input("Lorem @");
1626
1627        editor.update_in(&mut cx, |editor, window, cx| {
1628            assert_eq!(editor.text(cx), "Lorem @");
1629            assert!(editor.has_visible_completions_menu());
1630
1631            assert_eq!(
1632                current_completion_labels(editor),
1633                &[
1634                    format!("eight.txt b{slash}"),
1635                    format!("seven.txt b{slash}"),
1636                    format!("six.txt b{slash}"),
1637                    format!("five.txt b{slash}"),
1638                    "Files & Directories".into(),
1639                    "Symbols".into()
1640                ]
1641            );
1642            editor.set_text("", window, cx);
1643        });
1644
1645        prompt_capabilities.replace(
1646            acp::PromptCapabilities::new()
1647                .image(true)
1648                .audio(true)
1649                .embedded_context(true),
1650        );
1651
1652        cx.simulate_input("Lorem ");
1653
1654        editor.update(&mut cx, |editor, cx| {
1655            assert_eq!(editor.text(cx), "Lorem ");
1656            assert!(!editor.has_visible_completions_menu());
1657        });
1658
1659        cx.simulate_input("@");
1660
1661        editor.update(&mut cx, |editor, cx| {
1662            assert_eq!(editor.text(cx), "Lorem @");
1663            assert!(editor.has_visible_completions_menu());
1664            assert_eq!(
1665                current_completion_labels(editor),
1666                &[
1667                    format!("eight.txt b{slash}"),
1668                    format!("seven.txt b{slash}"),
1669                    format!("six.txt b{slash}"),
1670                    format!("five.txt b{slash}"),
1671                    "Files & Directories".into(),
1672                    "Symbols".into(),
1673                    "Threads".into(),
1674                    "Fetch".into()
1675                ]
1676            );
1677        });
1678
1679        // Select and confirm "File"
1680        editor.update_in(&mut cx, |editor, window, cx| {
1681            assert!(editor.has_visible_completions_menu());
1682            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1683            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1684            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1685            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1686            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1687        });
1688
1689        cx.run_until_parked();
1690
1691        editor.update(&mut cx, |editor, cx| {
1692            assert_eq!(editor.text(cx), "Lorem @file ");
1693            assert!(editor.has_visible_completions_menu());
1694        });
1695
1696        cx.simulate_input("one");
1697
1698        editor.update(&mut cx, |editor, cx| {
1699            assert_eq!(editor.text(cx), "Lorem @file one");
1700            assert!(editor.has_visible_completions_menu());
1701            assert_eq!(
1702                current_completion_labels(editor),
1703                vec![format!("one.txt a{slash}")]
1704            );
1705        });
1706
1707        editor.update_in(&mut cx, |editor, window, cx| {
1708            assert!(editor.has_visible_completions_menu());
1709            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1710        });
1711
1712        let url_one = MentionUri::File {
1713            abs_path: path!("/dir/a/one.txt").into(),
1714        }
1715        .to_uri()
1716        .to_string();
1717        editor.update(&mut cx, |editor, cx| {
1718            let text = editor.text(cx);
1719            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
1720            assert!(!editor.has_visible_completions_menu());
1721            assert_eq!(fold_ranges(editor, cx).len(), 1);
1722        });
1723
1724        let contents = message_editor
1725            .update(&mut cx, |message_editor, cx| {
1726                message_editor
1727                    .mention_set()
1728                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1729            })
1730            .await
1731            .unwrap()
1732            .into_values()
1733            .collect::<Vec<_>>();
1734
1735        {
1736            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
1737                panic!("Unexpected mentions");
1738            };
1739            pretty_assertions::assert_eq!(content, "1");
1740            pretty_assertions::assert_eq!(
1741                uri,
1742                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
1743            );
1744        }
1745
1746        cx.simulate_input(" ");
1747
1748        editor.update(&mut cx, |editor, cx| {
1749            let text = editor.text(cx);
1750            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
1751            assert!(!editor.has_visible_completions_menu());
1752            assert_eq!(fold_ranges(editor, cx).len(), 1);
1753        });
1754
1755        cx.simulate_input("Ipsum ");
1756
1757        editor.update(&mut cx, |editor, cx| {
1758            let text = editor.text(cx);
1759            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
1760            assert!(!editor.has_visible_completions_menu());
1761            assert_eq!(fold_ranges(editor, cx).len(), 1);
1762        });
1763
1764        cx.simulate_input("@file ");
1765
1766        editor.update(&mut cx, |editor, cx| {
1767            let text = editor.text(cx);
1768            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
1769            assert!(editor.has_visible_completions_menu());
1770            assert_eq!(fold_ranges(editor, cx).len(), 1);
1771        });
1772
1773        editor.update_in(&mut cx, |editor, window, cx| {
1774            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1775        });
1776
1777        cx.run_until_parked();
1778
1779        let contents = message_editor
1780            .update(&mut cx, |message_editor, cx| {
1781                message_editor
1782                    .mention_set()
1783                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1784            })
1785            .await
1786            .unwrap()
1787            .into_values()
1788            .collect::<Vec<_>>();
1789
1790        let url_eight = MentionUri::File {
1791            abs_path: path!("/dir/b/eight.txt").into(),
1792        }
1793        .to_uri()
1794        .to_string();
1795
1796        {
1797            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
1798                panic!("Unexpected mentions");
1799            };
1800            pretty_assertions::assert_eq!(content, "8");
1801            pretty_assertions::assert_eq!(
1802                uri,
1803                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
1804            );
1805        }
1806
1807        editor.update(&mut cx, |editor, cx| {
1808            assert_eq!(
1809                editor.text(cx),
1810                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
1811            );
1812            assert!(!editor.has_visible_completions_menu());
1813            assert_eq!(fold_ranges(editor, cx).len(), 2);
1814        });
1815
1816        let plain_text_language = Arc::new(language::Language::new(
1817            language::LanguageConfig {
1818                name: "Plain Text".into(),
1819                matcher: language::LanguageMatcher {
1820                    path_suffixes: vec!["txt".to_string()],
1821                    ..Default::default()
1822                },
1823                ..Default::default()
1824            },
1825            None,
1826        ));
1827
1828        // Register the language and fake LSP
1829        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1830        language_registry.add(plain_text_language);
1831
1832        let mut fake_language_servers = language_registry.register_fake_lsp(
1833            "Plain Text",
1834            language::FakeLspAdapter {
1835                capabilities: lsp::ServerCapabilities {
1836                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1837                    ..Default::default()
1838                },
1839                ..Default::default()
1840            },
1841        );
1842
1843        // Open the buffer to trigger LSP initialization
1844        let buffer = project
1845            .update(&mut cx, |project, cx| {
1846                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1847            })
1848            .await
1849            .unwrap();
1850
1851        // Register the buffer with language servers
1852        let _handle = project.update(&mut cx, |project, cx| {
1853            project.register_buffer_with_language_servers(&buffer, cx)
1854        });
1855
1856        cx.run_until_parked();
1857
1858        let fake_language_server = fake_language_servers.next().await.unwrap();
1859        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1860            move |_, _| async move {
1861                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1862                    #[allow(deprecated)]
1863                    lsp::SymbolInformation {
1864                        name: "MySymbol".into(),
1865                        location: lsp::Location {
1866                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1867                            range: lsp::Range::new(
1868                                lsp::Position::new(0, 0),
1869                                lsp::Position::new(0, 1),
1870                            ),
1871                        },
1872                        kind: lsp::SymbolKind::CONSTANT,
1873                        tags: None,
1874                        container_name: None,
1875                        deprecated: None,
1876                    },
1877                ])))
1878            },
1879        );
1880
1881        cx.simulate_input("@symbol ");
1882
1883        editor.update(&mut cx, |editor, cx| {
1884            assert_eq!(
1885                editor.text(cx),
1886                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
1887            );
1888            assert!(editor.has_visible_completions_menu());
1889            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
1890        });
1891
1892        editor.update_in(&mut cx, |editor, window, cx| {
1893            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1894        });
1895
1896        let symbol = MentionUri::Symbol {
1897            abs_path: path!("/dir/a/one.txt").into(),
1898            name: "MySymbol".into(),
1899            line_range: 0..=0,
1900        };
1901
1902        let contents = message_editor
1903            .update(&mut cx, |message_editor, cx| {
1904                message_editor
1905                    .mention_set()
1906                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1907            })
1908            .await
1909            .unwrap()
1910            .into_values()
1911            .collect::<Vec<_>>();
1912
1913        {
1914            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
1915                panic!("Unexpected mentions");
1916            };
1917            pretty_assertions::assert_eq!(content, "1");
1918            pretty_assertions::assert_eq!(uri, &symbol);
1919        }
1920
1921        cx.run_until_parked();
1922
1923        editor.read_with(&cx, |editor, cx| {
1924            assert_eq!(
1925                editor.text(cx),
1926                format!(
1927                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1928                    symbol.to_uri(),
1929                )
1930            );
1931        });
1932
1933        // Try to mention an "image" file that will fail to load
1934        cx.simulate_input("@file x.png");
1935
1936        editor.update(&mut cx, |editor, cx| {
1937            assert_eq!(
1938                editor.text(cx),
1939                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
1940            );
1941            assert!(editor.has_visible_completions_menu());
1942            assert_eq!(current_completion_labels(editor), &["x.png "]);
1943        });
1944
1945        editor.update_in(&mut cx, |editor, window, cx| {
1946            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1947        });
1948
1949        // Getting the message contents fails
1950        message_editor
1951            .update(&mut cx, |message_editor, cx| {
1952                message_editor
1953                    .mention_set()
1954                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1955            })
1956            .await
1957            .expect_err("Should fail to load x.png");
1958
1959        cx.run_until_parked();
1960
1961        // Mention was removed
1962        editor.read_with(&cx, |editor, cx| {
1963            assert_eq!(
1964                editor.text(cx),
1965                format!(
1966                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1967                    symbol.to_uri()
1968                )
1969            );
1970        });
1971
1972        // Once more
1973        cx.simulate_input("@file x.png");
1974
1975        editor.update(&mut cx, |editor, cx| {
1976                    assert_eq!(
1977                        editor.text(cx),
1978                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
1979                    );
1980                    assert!(editor.has_visible_completions_menu());
1981                    assert_eq!(current_completion_labels(editor), &["x.png "]);
1982                });
1983
1984        editor.update_in(&mut cx, |editor, window, cx| {
1985            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1986        });
1987
1988        // This time don't immediately get the contents, just let the confirmed completion settle
1989        cx.run_until_parked();
1990
1991        // Mention was removed
1992        editor.read_with(&cx, |editor, cx| {
1993            assert_eq!(
1994                editor.text(cx),
1995                format!(
1996                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1997                    symbol.to_uri()
1998                )
1999            );
2000        });
2001
2002        // Now getting the contents succeeds, because the invalid mention was removed
2003        let contents = message_editor
2004            .update(&mut cx, |message_editor, cx| {
2005                message_editor
2006                    .mention_set()
2007                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
2008            })
2009            .await
2010            .unwrap();
2011        assert_eq!(contents.len(), 3);
2012    }
2013
2014    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2015        let snapshot = editor.buffer().read(cx).snapshot(cx);
2016        editor.display_map.update(cx, |display_map, cx| {
2017            display_map
2018                .snapshot(cx)
2019                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
2020                .map(|fold| fold.range.to_point(&snapshot))
2021                .collect()
2022        })
2023    }
2024
2025    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2026        let completions = editor.current_completions().expect("Missing completions");
2027        completions
2028            .into_iter()
2029            .map(|completion| completion.label.text)
2030            .collect::<Vec<_>>()
2031    }
2032
2033    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2034        let completions = editor.current_completions().expect("Missing completions");
2035        completions
2036            .into_iter()
2037            .map(|completion| {
2038                (
2039                    completion.label.text,
2040                    completion
2041                        .documentation
2042                        .map(|d| d.text().to_string())
2043                        .unwrap_or_default(),
2044                )
2045            })
2046            .collect::<Vec<_>>()
2047    }
2048
2049    #[gpui::test]
2050    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
2051        init_test(cx);
2052
2053        let fs = FakeFs::new(cx.executor());
2054
2055        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2056        // Using plain text without a configured language, so no outline is available
2057        const LINE: &str = "This is a line of text in the file\n";
2058        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2059        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2060
2061        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2062        let small_content = "fn small_function() { /* small */ }\n";
2063        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2064
2065        fs.insert_tree(
2066            "/project",
2067            json!({
2068                "large_file.txt": large_content.clone(),
2069                "small_file.txt": small_content,
2070            }),
2071        )
2072        .await;
2073
2074        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2075
2076        let (workspace, cx) =
2077            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2078
2079        let history_store = cx.new(|cx| ThreadStore::new(cx));
2080
2081        let message_editor = cx.update(|window, cx| {
2082            cx.new(|cx| {
2083                let editor = MessageEditor::new(
2084                    workspace.downgrade(),
2085                    project.downgrade(),
2086                    history_store.clone(),
2087                    None,
2088                    Default::default(),
2089                    Default::default(),
2090                    "Test Agent".into(),
2091                    "Test",
2092                    EditorMode::AutoHeight {
2093                        min_lines: 1,
2094                        max_lines: None,
2095                    },
2096                    window,
2097                    cx,
2098                );
2099                // Enable embedded context so files are actually included
2100                editor
2101                    .prompt_capabilities
2102                    .replace(acp::PromptCapabilities::new().embedded_context(true));
2103                editor
2104            })
2105        });
2106
2107        // Test large file mention
2108        // Get the absolute path using the project's worktree
2109        let large_file_abs_path = project.read_with(cx, |project, cx| {
2110            let worktree = project.worktrees(cx).next().unwrap();
2111            let worktree_root = worktree.read(cx).abs_path();
2112            worktree_root.join("large_file.txt")
2113        });
2114        let large_file_task = message_editor.update(cx, |editor, cx| {
2115            editor.mention_set().update(cx, |set, cx| {
2116                set.confirm_mention_for_file(large_file_abs_path, true, cx)
2117            })
2118        });
2119
2120        let large_file_mention = large_file_task.await.unwrap();
2121        match large_file_mention {
2122            Mention::Text { content, .. } => {
2123                // Should contain some of the content but not all of it
2124                assert!(
2125                    content.contains(LINE),
2126                    "Should contain some of the file content"
2127                );
2128                assert!(
2129                    !content.contains(&LINE.repeat(100)),
2130                    "Should not contain the full file"
2131                );
2132                // Should be much smaller than original
2133                assert!(
2134                    content.len() < large_content.len() / 10,
2135                    "Should be significantly truncated"
2136                );
2137            }
2138            _ => panic!("Expected Text mention for large file"),
2139        }
2140
2141        // Test small file mention
2142        // Get the absolute path using the project's worktree
2143        let small_file_abs_path = project.read_with(cx, |project, cx| {
2144            let worktree = project.worktrees(cx).next().unwrap();
2145            let worktree_root = worktree.read(cx).abs_path();
2146            worktree_root.join("small_file.txt")
2147        });
2148        let small_file_task = message_editor.update(cx, |editor, cx| {
2149            editor.mention_set().update(cx, |set, cx| {
2150                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2151            })
2152        });
2153
2154        let small_file_mention = small_file_task.await.unwrap();
2155        match small_file_mention {
2156            Mention::Text { content, .. } => {
2157                // Should contain the full actual content
2158                assert_eq!(content, small_content);
2159            }
2160            _ => panic!("Expected Text mention for small file"),
2161        }
2162    }
2163
2164    #[gpui::test]
2165    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2166        init_test(cx);
2167        cx.update(LanguageModelRegistry::test);
2168
2169        let fs = FakeFs::new(cx.executor());
2170        fs.insert_tree("/project", json!({"file": ""})).await;
2171        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2172
2173        let (workspace, cx) =
2174            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2175
2176        let history_store = cx.new(|cx| ThreadStore::new(cx));
2177
2178        // Create a thread metadata to insert as summary
2179        let thread_metadata = agent::DbThreadMetadata {
2180            id: acp::SessionId::new("thread-123"),
2181            title: "Previous Conversation".into(),
2182            updated_at: chrono::Utc::now(),
2183        };
2184
2185        let message_editor = cx.update(|window, cx| {
2186            cx.new(|cx| {
2187                let mut editor = MessageEditor::new(
2188                    workspace.downgrade(),
2189                    project.downgrade(),
2190                    history_store.clone(),
2191                    None,
2192                    Default::default(),
2193                    Default::default(),
2194                    "Test Agent".into(),
2195                    "Test",
2196                    EditorMode::AutoHeight {
2197                        min_lines: 1,
2198                        max_lines: None,
2199                    },
2200                    window,
2201                    cx,
2202                );
2203                editor.insert_thread_summary(thread_metadata.clone(), window, cx);
2204                editor
2205            })
2206        });
2207
2208        // Construct expected values for verification
2209        let expected_uri = MentionUri::Thread {
2210            id: thread_metadata.id.clone(),
2211            name: thread_metadata.title.to_string(),
2212        };
2213        let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri());
2214
2215        message_editor.read_with(cx, |editor, cx| {
2216            let text = editor.text(cx);
2217
2218            assert!(
2219                text.contains(&expected_link),
2220                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2221                expected_link,
2222                text
2223            );
2224
2225            let mentions = editor.mention_set().read(cx).mentions();
2226            assert_eq!(
2227                mentions.len(),
2228                1,
2229                "Expected exactly one mention after inserting thread summary"
2230            );
2231
2232            assert!(
2233                mentions.contains(&expected_uri),
2234                "Expected mentions to contain the thread URI"
2235            );
2236        });
2237    }
2238
2239    #[gpui::test]
2240    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
2241        init_test(cx);
2242
2243        let fs = FakeFs::new(cx.executor());
2244        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
2245            .await;
2246        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2247
2248        let (workspace, cx) =
2249            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2250
2251        let history_store = cx.new(|cx| ThreadStore::new(cx));
2252
2253        let message_editor = cx.update(|window, cx| {
2254            cx.new(|cx| {
2255                MessageEditor::new(
2256                    workspace.downgrade(),
2257                    project.downgrade(),
2258                    history_store.clone(),
2259                    None,
2260                    Default::default(),
2261                    Default::default(),
2262                    "Test Agent".into(),
2263                    "Test",
2264                    EditorMode::AutoHeight {
2265                        min_lines: 1,
2266                        max_lines: None,
2267                    },
2268                    window,
2269                    cx,
2270                )
2271            })
2272        });
2273        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2274
2275        cx.run_until_parked();
2276
2277        editor.update_in(cx, |editor, window, cx| {
2278            editor.set_text("  \u{A0}してhello world  ", window, cx);
2279        });
2280
2281        let (content, _) = message_editor
2282            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2283            .await
2284            .unwrap();
2285
2286        assert_eq!(content, vec!["してhello world".into()]);
2287    }
2288
2289    #[gpui::test]
2290    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
2291        init_test(cx);
2292
2293        let fs = FakeFs::new(cx.executor());
2294
2295        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
2296
2297        fs.insert_tree(
2298            "/project",
2299            json!({
2300                "src": {
2301                    "main.rs": file_content,
2302                }
2303            }),
2304        )
2305        .await;
2306
2307        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2308
2309        let (workspace, cx) =
2310            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2311
2312        let history_store = cx.new(|cx| ThreadStore::new(cx));
2313
2314        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
2315            let workspace_handle = cx.weak_entity();
2316            let message_editor = cx.new(|cx| {
2317                MessageEditor::new(
2318                    workspace_handle,
2319                    project.downgrade(),
2320                    history_store.clone(),
2321                    None,
2322                    Default::default(),
2323                    Default::default(),
2324                    "Test Agent".into(),
2325                    "Test",
2326                    EditorMode::AutoHeight {
2327                        max_lines: None,
2328                        min_lines: 1,
2329                    },
2330                    window,
2331                    cx,
2332                )
2333            });
2334            workspace.active_pane().update(cx, |pane, cx| {
2335                pane.add_item(
2336                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2337                    true,
2338                    true,
2339                    None,
2340                    window,
2341                    cx,
2342                );
2343            });
2344            message_editor.read(cx).focus_handle(cx).focus(window, cx);
2345            let editor = message_editor.read(cx).editor().clone();
2346            (message_editor, editor)
2347        });
2348
2349        cx.simulate_input("What is in @file main");
2350
2351        editor.update_in(cx, |editor, window, cx| {
2352            assert!(editor.has_visible_completions_menu());
2353            assert_eq!(editor.text(cx), "What is in @file main");
2354            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2355        });
2356
2357        let content = message_editor
2358            .update(cx, |editor, cx| editor.contents(false, cx))
2359            .await
2360            .unwrap()
2361            .0;
2362
2363        let main_rs_uri = if cfg!(windows) {
2364            "file:///C:/project/src/main.rs"
2365        } else {
2366            "file:///project/src/main.rs"
2367        };
2368
2369        // When embedded context is `false` we should get a resource link
2370        pretty_assertions::assert_eq!(
2371            content,
2372            vec![
2373                "What is in ".into(),
2374                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
2375            ]
2376        );
2377
2378        message_editor.update(cx, |editor, _cx| {
2379            editor
2380                .prompt_capabilities
2381                .replace(acp::PromptCapabilities::new().embedded_context(true))
2382        });
2383
2384        let content = message_editor
2385            .update(cx, |editor, cx| editor.contents(false, cx))
2386            .await
2387            .unwrap()
2388            .0;
2389
2390        // When embedded context is `true` we should get a resource
2391        pretty_assertions::assert_eq!(
2392            content,
2393            vec![
2394                "What is in ".into(),
2395                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
2396                    acp::EmbeddedResourceResource::TextResourceContents(
2397                        acp::TextResourceContents::new(file_content, main_rs_uri)
2398                    )
2399                ))
2400            ]
2401        );
2402    }
2403
2404    #[gpui::test]
2405    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
2406        init_test(cx);
2407
2408        let app_state = cx.update(AppState::test);
2409
2410        cx.update(|cx| {
2411            editor::init(cx);
2412            workspace::init(app_state.clone(), cx);
2413        });
2414
2415        app_state
2416            .fs
2417            .as_fake()
2418            .insert_tree(
2419                path!("/dir"),
2420                json!({
2421                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
2422                }),
2423            )
2424            .await;
2425
2426        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2427        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2428        let workspace = window.root(cx).unwrap();
2429
2430        let worktree = project.update(cx, |project, cx| {
2431            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2432            assert_eq!(worktrees.len(), 1);
2433            worktrees.pop().unwrap()
2434        });
2435        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2436
2437        let mut cx = VisualTestContext::from_window(*window, cx);
2438
2439        // Open a regular editor with the created file, and select a portion of
2440        // the text that will be used for the selections that are meant to be
2441        // inserted in the agent panel.
2442        let editor = workspace
2443            .update_in(&mut cx, |workspace, window, cx| {
2444                workspace.open_path(
2445                    ProjectPath {
2446                        worktree_id,
2447                        path: rel_path("test.txt").into(),
2448                    },
2449                    None,
2450                    false,
2451                    window,
2452                    cx,
2453                )
2454            })
2455            .await
2456            .unwrap()
2457            .downcast::<Editor>()
2458            .unwrap();
2459
2460        editor.update_in(&mut cx, |editor, window, cx| {
2461            editor.change_selections(Default::default(), window, cx, |selections| {
2462                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
2463            });
2464        });
2465
2466        let history_store = cx.new(|cx| ThreadStore::new(cx));
2467
2468        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
2469        // to ensure we have a fixed viewport, so we can eventually actually
2470        // place the cursor outside of the visible area.
2471        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2472            let workspace_handle = cx.weak_entity();
2473            let message_editor = cx.new(|cx| {
2474                MessageEditor::new(
2475                    workspace_handle,
2476                    project.downgrade(),
2477                    history_store.clone(),
2478                    None,
2479                    Default::default(),
2480                    Default::default(),
2481                    "Test Agent".into(),
2482                    "Test",
2483                    EditorMode::full(),
2484                    window,
2485                    cx,
2486                )
2487            });
2488            workspace.active_pane().update(cx, |pane, cx| {
2489                pane.add_item(
2490                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2491                    true,
2492                    true,
2493                    None,
2494                    window,
2495                    cx,
2496                );
2497            });
2498
2499            message_editor
2500        });
2501
2502        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2503            message_editor.editor.update(cx, |editor, cx| {
2504                // Update the Agent Panel's Message Editor text to have 100
2505                // lines, ensuring that the cursor is set at line 90 and that we
2506                // then scroll all the way to the top, so the cursor's position
2507                // remains off screen.
2508                let mut lines = String::new();
2509                for _ in 1..=100 {
2510                    lines.push_str(&"Another line in the agent panel's message editor\n");
2511                }
2512                editor.set_text(lines.as_str(), window, cx);
2513                editor.change_selections(Default::default(), window, cx, |selections| {
2514                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
2515                });
2516                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
2517            });
2518        });
2519
2520        cx.run_until_parked();
2521
2522        // Before proceeding, let's assert that the cursor is indeed off screen,
2523        // otherwise the rest of the test doesn't make sense.
2524        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2525            message_editor.editor.update(cx, |editor, cx| {
2526                let snapshot = editor.snapshot(window, cx);
2527                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
2528                let scroll_top = snapshot.scroll_position().y as u32;
2529                let visible_lines = editor.visible_line_count().unwrap() as u32;
2530                let visible_range = scroll_top..(scroll_top + visible_lines);
2531
2532                assert!(!visible_range.contains(&cursor_row));
2533            })
2534        });
2535
2536        // Now let's insert the selection in the Agent Panel's editor and
2537        // confirm that, after the insertion, the cursor is now in the visible
2538        // range.
2539        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2540            message_editor.insert_selections(window, cx);
2541        });
2542
2543        cx.run_until_parked();
2544
2545        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2546            message_editor.editor.update(cx, |editor, cx| {
2547                let snapshot = editor.snapshot(window, cx);
2548                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
2549                let scroll_top = snapshot.scroll_position().y as u32;
2550                let visible_lines = editor.visible_line_count().unwrap() as u32;
2551                let visible_range = scroll_top..(scroll_top + visible_lines);
2552
2553                assert!(visible_range.contains(&cursor_row));
2554            })
2555        });
2556    }
2557}