message_editor.rs

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