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 insertion_target = self
 571                    .editor
 572                    .read(cx)
 573                    .selections
 574                    .newest_anchor()
 575                    .start
 576                    .text_anchor;
 577
 578                let project = workspace.read(cx).project().clone();
 579                for selection in selections {
 580                    if let (Some(file_path), Some(line_range)) =
 581                        (selection.file_path, selection.line_range)
 582                    {
 583                        let crease_text =
 584                            acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
 585
 586                        let mention_uri = MentionUri::Selection {
 587                            abs_path: Some(file_path.clone()),
 588                            line_range: line_range.clone(),
 589                        };
 590
 591                        let mention_text = mention_uri.as_link().to_string();
 592                        let (excerpt_id, text_anchor, content_len) =
 593                            self.editor.update(cx, |editor, cx| {
 594                                let buffer = editor.buffer().read(cx);
 595                                let snapshot = buffer.snapshot(cx);
 596                                let (excerpt_id, _, buffer_snapshot) =
 597                                    snapshot.as_singleton().unwrap();
 598                                let text_anchor = insertion_target.bias_left(&buffer_snapshot);
 599
 600                                editor.insert(&mention_text, window, cx);
 601                                editor.insert(" ", window, cx);
 602
 603                                (*excerpt_id, text_anchor, mention_text.len())
 604                            });
 605
 606                        let Some((crease_id, tx)) = insert_crease_for_mention(
 607                            excerpt_id,
 608                            text_anchor,
 609                            content_len,
 610                            crease_text.into(),
 611                            mention_uri.icon_path(cx),
 612                            None,
 613                            self.editor.clone(),
 614                            window,
 615                            cx,
 616                        ) else {
 617                            continue;
 618                        };
 619                        drop(tx);
 620
 621                        let mention_task = cx
 622                            .spawn({
 623                                let project = project.clone();
 624                                async move |_, cx| {
 625                                    let project_path = project
 626                                        .update(cx, |project, cx| {
 627                                            project.project_path_for_absolute_path(&file_path, cx)
 628                                        })
 629                                        .map_err(|e| e.to_string())?
 630                                        .ok_or_else(|| "project path not found".to_string())?;
 631
 632                                    let buffer = project
 633                                        .update(cx, |project, cx| {
 634                                            project.open_buffer(project_path, cx)
 635                                        })
 636                                        .map_err(|e| e.to_string())?
 637                                        .await
 638                                        .map_err(|e| e.to_string())?;
 639
 640                                    buffer
 641                                        .update(cx, |buffer, cx| {
 642                                            let start = Point::new(*line_range.start(), 0)
 643                                                .min(buffer.max_point());
 644                                            let end = Point::new(*line_range.end() + 1, 0)
 645                                                .min(buffer.max_point());
 646                                            let content =
 647                                                buffer.text_for_range(start..end).collect();
 648                                            Mention::Text {
 649                                                content,
 650                                                tracked_buffers: vec![cx.entity()],
 651                                            }
 652                                        })
 653                                        .map_err(|e| e.to_string())
 654                                }
 655                            })
 656                            .shared();
 657
 658                        self.mention_set.update(cx, |mention_set, _cx| {
 659                            mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
 660                        });
 661                    }
 662                }
 663                return;
 664            }
 665        }
 666
 667        if self.prompt_capabilities.borrow().image
 668            && let Some(task) =
 669                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
 670        {
 671            task.detach();
 672        }
 673    }
 674
 675    pub fn insert_dragged_files(
 676        &mut self,
 677        paths: Vec<project::ProjectPath>,
 678        added_worktrees: Vec<Entity<Worktree>>,
 679        window: &mut Window,
 680        cx: &mut Context<Self>,
 681    ) {
 682        let Some(workspace) = self.workspace.upgrade() else {
 683            return;
 684        };
 685        let project = workspace.read(cx).project().clone();
 686        let path_style = project.read(cx).path_style(cx);
 687        let buffer = self.editor.read(cx).buffer().clone();
 688        let Some(buffer) = buffer.read(cx).as_singleton() else {
 689            return;
 690        };
 691        let mut tasks = Vec::new();
 692        for path in paths {
 693            let Some(entry) = project.read(cx).entry_for_path(&path, cx) else {
 694                continue;
 695            };
 696            let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else {
 697                continue;
 698            };
 699            let abs_path = worktree.read(cx).absolutize(&path.path);
 700            let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
 701                &path.path,
 702                worktree.read(cx).root_name(),
 703                path_style,
 704            );
 705
 706            let uri = if entry.is_dir() {
 707                MentionUri::Directory { abs_path }
 708            } else {
 709                MentionUri::File { abs_path }
 710            };
 711
 712            let new_text = format!("{} ", uri.as_link());
 713            let content_len = new_text.len() - 1;
 714
 715            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 716
 717            self.editor.update(cx, |message_editor, cx| {
 718                message_editor.edit(
 719                    [(
 720                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 721                        new_text,
 722                    )],
 723                    cx,
 724                );
 725            });
 726            let supports_images = self.prompt_capabilities.borrow().image;
 727            tasks.push(self.mention_set.update(cx, |mention_set, cx| {
 728                mention_set.confirm_mention_completion(
 729                    file_name,
 730                    anchor,
 731                    content_len,
 732                    uri,
 733                    supports_images,
 734                    self.editor.clone(),
 735                    &workspace,
 736                    window,
 737                    cx,
 738                )
 739            }));
 740        }
 741        cx.spawn(async move |_, _| {
 742            join_all(tasks).await;
 743            drop(added_worktrees);
 744        })
 745        .detach();
 746    }
 747
 748    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 749        let editor = self.editor.read(cx);
 750        let editor_buffer = editor.buffer().read(cx);
 751        let Some(buffer) = editor_buffer.as_singleton() else {
 752            return;
 753        };
 754        let cursor_anchor = editor.selections.newest_anchor().head();
 755        let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
 756        let anchor = buffer.update(cx, |buffer, _cx| {
 757            buffer.anchor_before(cursor_offset.0.min(buffer.len()))
 758        });
 759        let Some(workspace) = self.workspace.upgrade() else {
 760            return;
 761        };
 762        let Some(completion) =
 763            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
 764                PromptContextAction::AddSelections,
 765                anchor..anchor,
 766                self.editor.downgrade(),
 767                self.mention_set.downgrade(),
 768                &workspace,
 769                cx,
 770            )
 771        else {
 772            return;
 773        };
 774
 775        self.editor.update(cx, |message_editor, cx| {
 776            message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
 777            message_editor.request_autoscroll(Autoscroll::fit(), cx);
 778        });
 779        if let Some(confirm) = completion.confirm {
 780            confirm(CompletionIntent::Complete, window, cx);
 781        }
 782    }
 783
 784    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
 785        self.editor.update(cx, |message_editor, cx| {
 786            message_editor.set_read_only(read_only);
 787            cx.notify()
 788        })
 789    }
 790
 791    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
 792        self.editor.update(cx, |editor, cx| {
 793            editor.set_mode(mode);
 794            cx.notify()
 795        });
 796    }
 797
 798    pub fn set_message(
 799        &mut self,
 800        message: Vec<acp::ContentBlock>,
 801        window: &mut Window,
 802        cx: &mut Context<Self>,
 803    ) {
 804        let Some(workspace) = self.workspace.upgrade() else {
 805            return;
 806        };
 807
 808        self.clear(window, cx);
 809
 810        let path_style = workspace.read(cx).project().read(cx).path_style(cx);
 811        let mut text = String::new();
 812        let mut mentions = Vec::new();
 813
 814        for chunk in message {
 815            match chunk {
 816                acp::ContentBlock::Text(text_content) => {
 817                    text.push_str(&text_content.text);
 818                }
 819                acp::ContentBlock::Resource(acp::EmbeddedResource {
 820                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
 821                    ..
 822                }) => {
 823                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
 824                    else {
 825                        continue;
 826                    };
 827                    let start = text.len();
 828                    write!(&mut text, "{}", mention_uri.as_link()).ok();
 829                    let end = text.len();
 830                    mentions.push((
 831                        start..end,
 832                        mention_uri,
 833                        Mention::Text {
 834                            content: resource.text,
 835                            tracked_buffers: Vec::new(),
 836                        },
 837                    ));
 838                }
 839                acp::ContentBlock::ResourceLink(resource) => {
 840                    if let Some(mention_uri) =
 841                        MentionUri::parse(&resource.uri, path_style).log_err()
 842                    {
 843                        let start = text.len();
 844                        write!(&mut text, "{}", mention_uri.as_link()).ok();
 845                        let end = text.len();
 846                        mentions.push((start..end, mention_uri, Mention::Link));
 847                    }
 848                }
 849                acp::ContentBlock::Image(acp::ImageContent {
 850                    uri,
 851                    data,
 852                    mime_type,
 853                    ..
 854                }) => {
 855                    let mention_uri = if let Some(uri) = uri {
 856                        MentionUri::parse(&uri, path_style)
 857                    } else {
 858                        Ok(MentionUri::PastedImage)
 859                    };
 860                    let Some(mention_uri) = mention_uri.log_err() else {
 861                        continue;
 862                    };
 863                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
 864                        log::error!("failed to parse MIME type for image: {mime_type:?}");
 865                        continue;
 866                    };
 867                    let start = text.len();
 868                    write!(&mut text, "{}", mention_uri.as_link()).ok();
 869                    let end = text.len();
 870                    mentions.push((
 871                        start..end,
 872                        mention_uri,
 873                        Mention::Image(MentionImage {
 874                            data: data.into(),
 875                            format,
 876                        }),
 877                    ));
 878                }
 879                _ => {}
 880            }
 881        }
 882
 883        let snapshot = self.editor.update(cx, |editor, cx| {
 884            editor.set_text(text, window, cx);
 885            editor.buffer().read(cx).snapshot(cx)
 886        });
 887
 888        for (range, mention_uri, mention) in mentions {
 889            let anchor = snapshot.anchor_before(MultiBufferOffset(range.start));
 890            let Some((crease_id, tx)) = insert_crease_for_mention(
 891                anchor.excerpt_id,
 892                anchor.text_anchor,
 893                range.end - range.start,
 894                mention_uri.name().into(),
 895                mention_uri.icon_path(cx),
 896                None,
 897                self.editor.clone(),
 898                window,
 899                cx,
 900            ) else {
 901                continue;
 902            };
 903            drop(tx);
 904
 905            self.mention_set.update(cx, |mention_set, _cx| {
 906                mention_set.insert_mention(
 907                    crease_id,
 908                    mention_uri.clone(),
 909                    Task::ready(Ok(mention)).shared(),
 910                )
 911            });
 912        }
 913        cx.notify();
 914    }
 915
 916    pub fn text(&self, cx: &App) -> String {
 917        self.editor.read(cx).text(cx)
 918    }
 919
 920    pub fn set_placeholder_text(
 921        &mut self,
 922        placeholder: &str,
 923        window: &mut Window,
 924        cx: &mut Context<Self>,
 925    ) {
 926        self.editor.update(cx, |editor, cx| {
 927            editor.set_placeholder_text(placeholder, window, cx);
 928        });
 929    }
 930
 931    #[cfg(test)]
 932    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
 933        self.editor.update(cx, |editor, cx| {
 934            editor.set_text(text, window, cx);
 935        });
 936    }
 937}
 938
 939impl Focusable for MessageEditor {
 940    fn focus_handle(&self, cx: &App) -> FocusHandle {
 941        self.editor.focus_handle(cx)
 942    }
 943}
 944
 945impl Render for MessageEditor {
 946    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 947        div()
 948            .key_context("MessageEditor")
 949            .on_action(cx.listener(Self::chat))
 950            .on_action(cx.listener(Self::chat_with_follow))
 951            .on_action(cx.listener(Self::cancel))
 952            .capture_action(cx.listener(Self::paste))
 953            .flex_1()
 954            .child({
 955                let settings = ThemeSettings::get_global(cx);
 956
 957                let text_style = TextStyle {
 958                    color: cx.theme().colors().text,
 959                    font_family: settings.buffer_font.family.clone(),
 960                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
 961                    font_features: settings.buffer_font.features.clone(),
 962                    font_size: settings.agent_buffer_font_size(cx).into(),
 963                    line_height: relative(settings.buffer_line_height.value()),
 964                    ..Default::default()
 965                };
 966
 967                EditorElement::new(
 968                    &self.editor,
 969                    EditorStyle {
 970                        background: cx.theme().colors().editor_background,
 971                        local_player: cx.theme().players().local(),
 972                        text: text_style,
 973                        syntax: cx.theme().syntax().clone(),
 974                        inlay_hints_style: editor::make_inlay_hints_style(cx),
 975                        ..Default::default()
 976                    },
 977                )
 978            })
 979    }
 980}
 981
 982pub struct MessageEditorAddon {}
 983
 984impl MessageEditorAddon {
 985    pub fn new() -> Self {
 986        Self {}
 987    }
 988}
 989
 990impl Addon for MessageEditorAddon {
 991    fn to_any(&self) -> &dyn std::any::Any {
 992        self
 993    }
 994
 995    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
 996        Some(self)
 997    }
 998
 999    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1000        let settings = agent_settings::AgentSettings::get_global(cx);
1001        if settings.use_modifier_to_send {
1002            key_context.add("use_modifier_to_send");
1003        }
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
1010
1011    use acp_thread::MentionUri;
1012    use agent::{HistoryStore, outline};
1013    use agent_client_protocol as acp;
1014    use assistant_text_thread::TextThreadStore;
1015    use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset};
1016    use fs::FakeFs;
1017    use futures::StreamExt as _;
1018    use gpui::{
1019        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1020    };
1021    use language_model::LanguageModelRegistry;
1022    use lsp::{CompletionContext, CompletionTriggerKind};
1023    use project::{CompletionIntent, Project, ProjectPath};
1024    use serde_json::json;
1025    use text::Point;
1026    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1027    use util::{path, paths::PathStyle, rel_path::rel_path};
1028    use workspace::{AppState, Item, Workspace};
1029
1030    use crate::acp::{
1031        message_editor::{Mention, MessageEditor},
1032        thread_view::tests::init_test,
1033    };
1034
1035    #[gpui::test]
1036    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1037        init_test(cx);
1038
1039        let fs = FakeFs::new(cx.executor());
1040        fs.insert_tree("/project", json!({"file": ""})).await;
1041        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1042
1043        let (workspace, cx) =
1044            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1045
1046        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1047        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1048
1049        let message_editor = cx.update(|window, cx| {
1050            cx.new(|cx| {
1051                MessageEditor::new(
1052                    workspace.downgrade(),
1053                    project.downgrade(),
1054                    history_store.clone(),
1055                    None,
1056                    Default::default(),
1057                    Default::default(),
1058                    "Test Agent".into(),
1059                    "Test",
1060                    EditorMode::AutoHeight {
1061                        min_lines: 1,
1062                        max_lines: None,
1063                    },
1064                    window,
1065                    cx,
1066                )
1067            })
1068        });
1069        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1070
1071        cx.run_until_parked();
1072
1073        let excerpt_id = editor.update(cx, |editor, cx| {
1074            editor
1075                .buffer()
1076                .read(cx)
1077                .excerpt_ids()
1078                .into_iter()
1079                .next()
1080                .unwrap()
1081        });
1082        let completions = editor.update_in(cx, |editor, window, cx| {
1083            editor.set_text("Hello @file ", window, cx);
1084            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1085            let completion_provider = editor.completion_provider().unwrap();
1086            completion_provider.completions(
1087                excerpt_id,
1088                &buffer,
1089                text::Anchor::MAX,
1090                CompletionContext {
1091                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1092                    trigger_character: Some("@".into()),
1093                },
1094                window,
1095                cx,
1096            )
1097        });
1098        let [_, completion]: [_; 2] = completions
1099            .await
1100            .unwrap()
1101            .into_iter()
1102            .flat_map(|response| response.completions)
1103            .collect::<Vec<_>>()
1104            .try_into()
1105            .unwrap();
1106
1107        editor.update_in(cx, |editor, window, cx| {
1108            let snapshot = editor.buffer().read(cx).snapshot(cx);
1109            let range = snapshot
1110                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
1111                .unwrap();
1112            editor.edit([(range, completion.new_text)], cx);
1113            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1114        });
1115
1116        cx.run_until_parked();
1117
1118        // Backspace over the inserted crease (and the following space).
1119        editor.update_in(cx, |editor, window, cx| {
1120            editor.backspace(&Default::default(), window, cx);
1121            editor.backspace(&Default::default(), window, cx);
1122        });
1123
1124        let (content, _) = message_editor
1125            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1126            .await
1127            .unwrap();
1128
1129        // We don't send a resource link for the deleted crease.
1130        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1131    }
1132
1133    #[gpui::test]
1134    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1135        init_test(cx);
1136        let fs = FakeFs::new(cx.executor());
1137        fs.insert_tree(
1138            "/test",
1139            json!({
1140                ".zed": {
1141                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1142                },
1143                "src": {
1144                    "main.rs": "fn main() {}",
1145                },
1146            }),
1147        )
1148        .await;
1149
1150        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1151        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1152        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1153        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1154        // Start with no available commands - simulating Claude which doesn't support slash commands
1155        let available_commands = Rc::new(RefCell::new(vec![]));
1156
1157        let (workspace, cx) =
1158            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1159        let workspace_handle = workspace.downgrade();
1160        let message_editor = workspace.update_in(cx, |_, window, cx| {
1161            cx.new(|cx| {
1162                MessageEditor::new(
1163                    workspace_handle.clone(),
1164                    project.downgrade(),
1165                    history_store.clone(),
1166                    None,
1167                    prompt_capabilities.clone(),
1168                    available_commands.clone(),
1169                    "Claude Code".into(),
1170                    "Test",
1171                    EditorMode::AutoHeight {
1172                        min_lines: 1,
1173                        max_lines: None,
1174                    },
1175                    window,
1176                    cx,
1177                )
1178            })
1179        });
1180        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1181
1182        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1183        editor.update_in(cx, |editor, window, cx| {
1184            editor.set_text("/file test.txt", window, cx);
1185        });
1186
1187        let contents_result = message_editor
1188            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1189            .await;
1190
1191        // Should fail because available_commands is empty (no commands supported)
1192        assert!(contents_result.is_err());
1193        let error_message = contents_result.unwrap_err().to_string();
1194        assert!(error_message.contains("not supported by Claude Code"));
1195        assert!(error_message.contains("Available commands: none"));
1196
1197        // Now simulate Claude providing its list of available commands (which doesn't include file)
1198        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
1199
1200        // Test that unsupported slash commands trigger an error when we have a list of available commands
1201        editor.update_in(cx, |editor, window, cx| {
1202            editor.set_text("/file test.txt", window, cx);
1203        });
1204
1205        let contents_result = message_editor
1206            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1207            .await;
1208
1209        assert!(contents_result.is_err());
1210        let error_message = contents_result.unwrap_err().to_string();
1211        assert!(error_message.contains("not supported by Claude Code"));
1212        assert!(error_message.contains("/file"));
1213        assert!(error_message.contains("Available commands: /help"));
1214
1215        // Test that supported commands work fine
1216        editor.update_in(cx, |editor, window, cx| {
1217            editor.set_text("/help", window, cx);
1218        });
1219
1220        let contents_result = message_editor
1221            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1222            .await;
1223
1224        // Should succeed because /help is in available_commands
1225        assert!(contents_result.is_ok());
1226
1227        // Test that regular text works fine
1228        editor.update_in(cx, |editor, window, cx| {
1229            editor.set_text("Hello Claude!", window, cx);
1230        });
1231
1232        let (content, _) = message_editor
1233            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1234            .await
1235            .unwrap();
1236
1237        assert_eq!(content.len(), 1);
1238        if let acp::ContentBlock::Text(text) = &content[0] {
1239            assert_eq!(text.text, "Hello Claude!");
1240        } else {
1241            panic!("Expected ContentBlock::Text");
1242        }
1243
1244        // Test that @ mentions still work
1245        editor.update_in(cx, |editor, window, cx| {
1246            editor.set_text("Check this @", window, cx);
1247        });
1248
1249        // The @ mention functionality should not be affected
1250        let (content, _) = message_editor
1251            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
1252            .await
1253            .unwrap();
1254
1255        assert_eq!(content.len(), 1);
1256        if let acp::ContentBlock::Text(text) = &content[0] {
1257            assert_eq!(text.text, "Check this @");
1258        } else {
1259            panic!("Expected ContentBlock::Text");
1260        }
1261    }
1262
1263    struct MessageEditorItem(Entity<MessageEditor>);
1264
1265    impl Item for MessageEditorItem {
1266        type Event = ();
1267
1268        fn include_in_nav_history() -> bool {
1269            false
1270        }
1271
1272        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1273            "Test".into()
1274        }
1275    }
1276
1277    impl EventEmitter<()> for MessageEditorItem {}
1278
1279    impl Focusable for MessageEditorItem {
1280        fn focus_handle(&self, cx: &App) -> FocusHandle {
1281            self.0.read(cx).focus_handle(cx)
1282        }
1283    }
1284
1285    impl Render for MessageEditorItem {
1286        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1287            self.0.clone().into_any_element()
1288        }
1289    }
1290
1291    #[gpui::test]
1292    async fn test_completion_provider_commands(cx: &mut TestAppContext) {
1293        init_test(cx);
1294
1295        let app_state = cx.update(AppState::test);
1296
1297        cx.update(|cx| {
1298            editor::init(cx);
1299            workspace::init(app_state.clone(), cx);
1300        });
1301
1302        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1303        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1304        let workspace = window.root(cx).unwrap();
1305
1306        let mut cx = VisualTestContext::from_window(*window, cx);
1307
1308        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1309        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1310        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1311        let available_commands = Rc::new(RefCell::new(vec![
1312            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
1313            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
1314                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
1315                    "<name>",
1316                )),
1317            ),
1318        ]));
1319
1320        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1321            let workspace_handle = cx.weak_entity();
1322            let message_editor = cx.new(|cx| {
1323                MessageEditor::new(
1324                    workspace_handle,
1325                    project.downgrade(),
1326                    history_store.clone(),
1327                    None,
1328                    prompt_capabilities.clone(),
1329                    available_commands.clone(),
1330                    "Test Agent".into(),
1331                    "Test",
1332                    EditorMode::AutoHeight {
1333                        max_lines: None,
1334                        min_lines: 1,
1335                    },
1336                    window,
1337                    cx,
1338                )
1339            });
1340            workspace.active_pane().update(cx, |pane, cx| {
1341                pane.add_item(
1342                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1343                    true,
1344                    true,
1345                    None,
1346                    window,
1347                    cx,
1348                );
1349            });
1350            message_editor.read(cx).focus_handle(cx).focus(window);
1351            message_editor.read(cx).editor().clone()
1352        });
1353
1354        cx.simulate_input("/");
1355
1356        editor.update_in(&mut cx, |editor, window, cx| {
1357            assert_eq!(editor.text(cx), "/");
1358            assert!(editor.has_visible_completions_menu());
1359
1360            assert_eq!(
1361                current_completion_labels_with_documentation(editor),
1362                &[
1363                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1364                    ("say-hello".into(), "Say hello to whoever you want".into())
1365                ]
1366            );
1367            editor.set_text("", window, cx);
1368        });
1369
1370        cx.simulate_input("/qui");
1371
1372        editor.update_in(&mut cx, |editor, window, cx| {
1373            assert_eq!(editor.text(cx), "/qui");
1374            assert!(editor.has_visible_completions_menu());
1375
1376            assert_eq!(
1377                current_completion_labels_with_documentation(editor),
1378                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1379            );
1380            editor.set_text("", window, cx);
1381        });
1382
1383        editor.update_in(&mut cx, |editor, window, cx| {
1384            assert!(editor.has_visible_completions_menu());
1385            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1386        });
1387
1388        cx.run_until_parked();
1389
1390        editor.update_in(&mut cx, |editor, window, cx| {
1391            assert_eq!(editor.display_text(cx), "/quick-math ");
1392            assert!(!editor.has_visible_completions_menu());
1393            editor.set_text("", window, cx);
1394        });
1395
1396        cx.simulate_input("/say");
1397
1398        editor.update_in(&mut cx, |editor, _window, cx| {
1399            assert_eq!(editor.display_text(cx), "/say");
1400            assert!(editor.has_visible_completions_menu());
1401
1402            assert_eq!(
1403                current_completion_labels_with_documentation(editor),
1404                &[("say-hello".into(), "Say hello to whoever you want".into())]
1405            );
1406        });
1407
1408        editor.update_in(&mut cx, |editor, window, cx| {
1409            assert!(editor.has_visible_completions_menu());
1410            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1411        });
1412
1413        cx.run_until_parked();
1414
1415        editor.update_in(&mut cx, |editor, _window, cx| {
1416            assert_eq!(editor.text(cx), "/say-hello ");
1417            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1418            assert!(!editor.has_visible_completions_menu());
1419        });
1420
1421        cx.simulate_input("GPT5");
1422
1423        cx.run_until_parked();
1424
1425        editor.update_in(&mut cx, |editor, window, cx| {
1426            assert_eq!(editor.text(cx), "/say-hello GPT5");
1427            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
1428            assert!(!editor.has_visible_completions_menu());
1429
1430            // Delete argument
1431            for _ in 0..5 {
1432                editor.backspace(&editor::actions::Backspace, window, cx);
1433            }
1434        });
1435
1436        cx.run_until_parked();
1437
1438        editor.update_in(&mut cx, |editor, window, cx| {
1439            assert_eq!(editor.text(cx), "/say-hello");
1440            // Hint is visible because argument was deleted
1441            assert_eq!(editor.display_text(cx), "/say-hello <name>");
1442
1443            // Delete last command letter
1444            editor.backspace(&editor::actions::Backspace, window, cx);
1445        });
1446
1447        cx.run_until_parked();
1448
1449        editor.update_in(&mut cx, |editor, _window, cx| {
1450            // Hint goes away once command no longer matches an available one
1451            assert_eq!(editor.text(cx), "/say-hell");
1452            assert_eq!(editor.display_text(cx), "/say-hell");
1453            assert!(!editor.has_visible_completions_menu());
1454        });
1455    }
1456
1457    #[gpui::test]
1458    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
1459        init_test(cx);
1460
1461        let app_state = cx.update(AppState::test);
1462
1463        cx.update(|cx| {
1464            editor::init(cx);
1465            workspace::init(app_state.clone(), cx);
1466        });
1467
1468        app_state
1469            .fs
1470            .as_fake()
1471            .insert_tree(
1472                path!("/dir"),
1473                json!({
1474                    "editor": "",
1475                    "a": {
1476                        "one.txt": "1",
1477                        "two.txt": "2",
1478                        "three.txt": "3",
1479                        "four.txt": "4"
1480                    },
1481                    "b": {
1482                        "five.txt": "5",
1483                        "six.txt": "6",
1484                        "seven.txt": "7",
1485                        "eight.txt": "8",
1486                    },
1487                    "x.png": "",
1488                }),
1489            )
1490            .await;
1491
1492        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1493        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1494        let workspace = window.root(cx).unwrap();
1495
1496        let worktree = project.update(cx, |project, cx| {
1497            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1498            assert_eq!(worktrees.len(), 1);
1499            worktrees.pop().unwrap()
1500        });
1501        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1502
1503        let mut cx = VisualTestContext::from_window(*window, cx);
1504
1505        let paths = vec![
1506            rel_path("a/one.txt"),
1507            rel_path("a/two.txt"),
1508            rel_path("a/three.txt"),
1509            rel_path("a/four.txt"),
1510            rel_path("b/five.txt"),
1511            rel_path("b/six.txt"),
1512            rel_path("b/seven.txt"),
1513            rel_path("b/eight.txt"),
1514        ];
1515
1516        let slash = PathStyle::local().primary_separator();
1517
1518        let mut opened_editors = Vec::new();
1519        for path in paths {
1520            let buffer = workspace
1521                .update_in(&mut cx, |workspace, window, cx| {
1522                    workspace.open_path(
1523                        ProjectPath {
1524                            worktree_id,
1525                            path: path.into(),
1526                        },
1527                        None,
1528                        false,
1529                        window,
1530                        cx,
1531                    )
1532                })
1533                .await
1534                .unwrap();
1535            opened_editors.push(buffer);
1536        }
1537
1538        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1539        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
1540        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
1541
1542        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1543            let workspace_handle = cx.weak_entity();
1544            let message_editor = cx.new(|cx| {
1545                MessageEditor::new(
1546                    workspace_handle,
1547                    project.downgrade(),
1548                    history_store.clone(),
1549                    None,
1550                    prompt_capabilities.clone(),
1551                    Default::default(),
1552                    "Test Agent".into(),
1553                    "Test",
1554                    EditorMode::AutoHeight {
1555                        max_lines: None,
1556                        min_lines: 1,
1557                    },
1558                    window,
1559                    cx,
1560                )
1561            });
1562            workspace.active_pane().update(cx, |pane, cx| {
1563                pane.add_item(
1564                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1565                    true,
1566                    true,
1567                    None,
1568                    window,
1569                    cx,
1570                );
1571            });
1572            message_editor.read(cx).focus_handle(cx).focus(window);
1573            let editor = message_editor.read(cx).editor().clone();
1574            (message_editor, editor)
1575        });
1576
1577        cx.simulate_input("Lorem @");
1578
1579        editor.update_in(&mut cx, |editor, window, cx| {
1580            assert_eq!(editor.text(cx), "Lorem @");
1581            assert!(editor.has_visible_completions_menu());
1582
1583            assert_eq!(
1584                current_completion_labels(editor),
1585                &[
1586                    format!("eight.txt b{slash}"),
1587                    format!("seven.txt b{slash}"),
1588                    format!("six.txt b{slash}"),
1589                    format!("five.txt b{slash}"),
1590                    "Files & Directories".into(),
1591                    "Symbols".into()
1592                ]
1593            );
1594            editor.set_text("", window, cx);
1595        });
1596
1597        prompt_capabilities.replace(
1598            acp::PromptCapabilities::new()
1599                .image(true)
1600                .audio(true)
1601                .embedded_context(true),
1602        );
1603
1604        cx.simulate_input("Lorem ");
1605
1606        editor.update(&mut cx, |editor, cx| {
1607            assert_eq!(editor.text(cx), "Lorem ");
1608            assert!(!editor.has_visible_completions_menu());
1609        });
1610
1611        cx.simulate_input("@");
1612
1613        editor.update(&mut cx, |editor, cx| {
1614            assert_eq!(editor.text(cx), "Lorem @");
1615            assert!(editor.has_visible_completions_menu());
1616            assert_eq!(
1617                current_completion_labels(editor),
1618                &[
1619                    format!("eight.txt b{slash}"),
1620                    format!("seven.txt b{slash}"),
1621                    format!("six.txt b{slash}"),
1622                    format!("five.txt b{slash}"),
1623                    "Files & Directories".into(),
1624                    "Symbols".into(),
1625                    "Threads".into(),
1626                    "Fetch".into()
1627                ]
1628            );
1629        });
1630
1631        // Select and confirm "File"
1632        editor.update_in(&mut cx, |editor, window, cx| {
1633            assert!(editor.has_visible_completions_menu());
1634            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1635            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1636            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1637            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1638            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1639        });
1640
1641        cx.run_until_parked();
1642
1643        editor.update(&mut cx, |editor, cx| {
1644            assert_eq!(editor.text(cx), "Lorem @file ");
1645            assert!(editor.has_visible_completions_menu());
1646        });
1647
1648        cx.simulate_input("one");
1649
1650        editor.update(&mut cx, |editor, cx| {
1651            assert_eq!(editor.text(cx), "Lorem @file one");
1652            assert!(editor.has_visible_completions_menu());
1653            assert_eq!(
1654                current_completion_labels(editor),
1655                vec![format!("one.txt a{slash}")]
1656            );
1657        });
1658
1659        editor.update_in(&mut cx, |editor, window, cx| {
1660            assert!(editor.has_visible_completions_menu());
1661            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1662        });
1663
1664        let url_one = MentionUri::File {
1665            abs_path: path!("/dir/a/one.txt").into(),
1666        }
1667        .to_uri()
1668        .to_string();
1669        editor.update(&mut cx, |editor, cx| {
1670            let text = editor.text(cx);
1671            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
1672            assert!(!editor.has_visible_completions_menu());
1673            assert_eq!(fold_ranges(editor, cx).len(), 1);
1674        });
1675
1676        let contents = message_editor
1677            .update(&mut cx, |message_editor, cx| {
1678                message_editor
1679                    .mention_set()
1680                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1681            })
1682            .await
1683            .unwrap()
1684            .into_values()
1685            .collect::<Vec<_>>();
1686
1687        {
1688            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
1689                panic!("Unexpected mentions");
1690            };
1691            pretty_assertions::assert_eq!(content, "1");
1692            pretty_assertions::assert_eq!(
1693                uri,
1694                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
1695            );
1696        }
1697
1698        cx.simulate_input(" ");
1699
1700        editor.update(&mut cx, |editor, cx| {
1701            let text = editor.text(cx);
1702            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
1703            assert!(!editor.has_visible_completions_menu());
1704            assert_eq!(fold_ranges(editor, cx).len(), 1);
1705        });
1706
1707        cx.simulate_input("Ipsum ");
1708
1709        editor.update(&mut cx, |editor, cx| {
1710            let text = editor.text(cx);
1711            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
1712            assert!(!editor.has_visible_completions_menu());
1713            assert_eq!(fold_ranges(editor, cx).len(), 1);
1714        });
1715
1716        cx.simulate_input("@file ");
1717
1718        editor.update(&mut cx, |editor, cx| {
1719            let text = editor.text(cx);
1720            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
1721            assert!(editor.has_visible_completions_menu());
1722            assert_eq!(fold_ranges(editor, cx).len(), 1);
1723        });
1724
1725        editor.update_in(&mut cx, |editor, window, cx| {
1726            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1727        });
1728
1729        cx.run_until_parked();
1730
1731        let contents = message_editor
1732            .update(&mut cx, |message_editor, cx| {
1733                message_editor
1734                    .mention_set()
1735                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1736            })
1737            .await
1738            .unwrap()
1739            .into_values()
1740            .collect::<Vec<_>>();
1741
1742        let url_eight = MentionUri::File {
1743            abs_path: path!("/dir/b/eight.txt").into(),
1744        }
1745        .to_uri()
1746        .to_string();
1747
1748        {
1749            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
1750                panic!("Unexpected mentions");
1751            };
1752            pretty_assertions::assert_eq!(content, "8");
1753            pretty_assertions::assert_eq!(
1754                uri,
1755                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
1756            );
1757        }
1758
1759        editor.update(&mut cx, |editor, cx| {
1760            assert_eq!(
1761                editor.text(cx),
1762                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
1763            );
1764            assert!(!editor.has_visible_completions_menu());
1765            assert_eq!(fold_ranges(editor, cx).len(), 2);
1766        });
1767
1768        let plain_text_language = Arc::new(language::Language::new(
1769            language::LanguageConfig {
1770                name: "Plain Text".into(),
1771                matcher: language::LanguageMatcher {
1772                    path_suffixes: vec!["txt".to_string()],
1773                    ..Default::default()
1774                },
1775                ..Default::default()
1776            },
1777            None,
1778        ));
1779
1780        // Register the language and fake LSP
1781        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1782        language_registry.add(plain_text_language);
1783
1784        let mut fake_language_servers = language_registry.register_fake_lsp(
1785            "Plain Text",
1786            language::FakeLspAdapter {
1787                capabilities: lsp::ServerCapabilities {
1788                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1789                    ..Default::default()
1790                },
1791                ..Default::default()
1792            },
1793        );
1794
1795        // Open the buffer to trigger LSP initialization
1796        let buffer = project
1797            .update(&mut cx, |project, cx| {
1798                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1799            })
1800            .await
1801            .unwrap();
1802
1803        // Register the buffer with language servers
1804        let _handle = project.update(&mut cx, |project, cx| {
1805            project.register_buffer_with_language_servers(&buffer, cx)
1806        });
1807
1808        cx.run_until_parked();
1809
1810        let fake_language_server = fake_language_servers.next().await.unwrap();
1811        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1812            move |_, _| async move {
1813                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1814                    #[allow(deprecated)]
1815                    lsp::SymbolInformation {
1816                        name: "MySymbol".into(),
1817                        location: lsp::Location {
1818                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1819                            range: lsp::Range::new(
1820                                lsp::Position::new(0, 0),
1821                                lsp::Position::new(0, 1),
1822                            ),
1823                        },
1824                        kind: lsp::SymbolKind::CONSTANT,
1825                        tags: None,
1826                        container_name: None,
1827                        deprecated: None,
1828                    },
1829                ])))
1830            },
1831        );
1832
1833        cx.simulate_input("@symbol ");
1834
1835        editor.update(&mut cx, |editor, cx| {
1836            assert_eq!(
1837                editor.text(cx),
1838                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
1839            );
1840            assert!(editor.has_visible_completions_menu());
1841            assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]);
1842        });
1843
1844        editor.update_in(&mut cx, |editor, window, cx| {
1845            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1846        });
1847
1848        let symbol = MentionUri::Symbol {
1849            abs_path: path!("/dir/a/one.txt").into(),
1850            name: "MySymbol".into(),
1851            line_range: 0..=0,
1852        };
1853
1854        let contents = message_editor
1855            .update(&mut cx, |message_editor, cx| {
1856                message_editor
1857                    .mention_set()
1858                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1859            })
1860            .await
1861            .unwrap()
1862            .into_values()
1863            .collect::<Vec<_>>();
1864
1865        {
1866            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
1867                panic!("Unexpected mentions");
1868            };
1869            pretty_assertions::assert_eq!(content, "1");
1870            pretty_assertions::assert_eq!(uri, &symbol);
1871        }
1872
1873        cx.run_until_parked();
1874
1875        editor.read_with(&cx, |editor, cx| {
1876            assert_eq!(
1877                editor.text(cx),
1878                format!(
1879                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1880                    symbol.to_uri(),
1881                )
1882            );
1883        });
1884
1885        // Try to mention an "image" file that will fail to load
1886        cx.simulate_input("@file x.png");
1887
1888        editor.update(&mut cx, |editor, cx| {
1889            assert_eq!(
1890                editor.text(cx),
1891                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
1892            );
1893            assert!(editor.has_visible_completions_menu());
1894            assert_eq!(current_completion_labels(editor), &["x.png "]);
1895        });
1896
1897        editor.update_in(&mut cx, |editor, window, cx| {
1898            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1899        });
1900
1901        // Getting the message contents fails
1902        message_editor
1903            .update(&mut cx, |message_editor, cx| {
1904                message_editor
1905                    .mention_set()
1906                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1907            })
1908            .await
1909            .expect_err("Should fail to load x.png");
1910
1911        cx.run_until_parked();
1912
1913        // Mention was removed
1914        editor.read_with(&cx, |editor, cx| {
1915            assert_eq!(
1916                editor.text(cx),
1917                format!(
1918                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1919                    symbol.to_uri()
1920                )
1921            );
1922        });
1923
1924        // Once more
1925        cx.simulate_input("@file x.png");
1926
1927        editor.update(&mut cx, |editor, cx| {
1928                    assert_eq!(
1929                        editor.text(cx),
1930                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
1931                    );
1932                    assert!(editor.has_visible_completions_menu());
1933                    assert_eq!(current_completion_labels(editor), &["x.png "]);
1934                });
1935
1936        editor.update_in(&mut cx, |editor, window, cx| {
1937            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1938        });
1939
1940        // This time don't immediately get the contents, just let the confirmed completion settle
1941        cx.run_until_parked();
1942
1943        // Mention was removed
1944        editor.read_with(&cx, |editor, cx| {
1945            assert_eq!(
1946                editor.text(cx),
1947                format!(
1948                    "Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
1949                    symbol.to_uri()
1950                )
1951            );
1952        });
1953
1954        // Now getting the contents succeeds, because the invalid mention was removed
1955        let contents = message_editor
1956            .update(&mut cx, |message_editor, cx| {
1957                message_editor
1958                    .mention_set()
1959                    .update(cx, |mention_set, cx| mention_set.contents(false, cx))
1960            })
1961            .await
1962            .unwrap();
1963        assert_eq!(contents.len(), 3);
1964    }
1965
1966    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1967        let snapshot = editor.buffer().read(cx).snapshot(cx);
1968        editor.display_map.update(cx, |display_map, cx| {
1969            display_map
1970                .snapshot(cx)
1971                .folds_in_range(MultiBufferOffset(0)..snapshot.len())
1972                .map(|fold| fold.range.to_point(&snapshot))
1973                .collect()
1974        })
1975    }
1976
1977    fn current_completion_labels(editor: &Editor) -> Vec<String> {
1978        let completions = editor.current_completions().expect("Missing completions");
1979        completions
1980            .into_iter()
1981            .map(|completion| completion.label.text)
1982            .collect::<Vec<_>>()
1983    }
1984
1985    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
1986        let completions = editor.current_completions().expect("Missing completions");
1987        completions
1988            .into_iter()
1989            .map(|completion| {
1990                (
1991                    completion.label.text,
1992                    completion
1993                        .documentation
1994                        .map(|d| d.text().to_string())
1995                        .unwrap_or_default(),
1996                )
1997            })
1998            .collect::<Vec<_>>()
1999    }
2000
2001    #[gpui::test]
2002    async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
2003        init_test(cx);
2004
2005        let fs = FakeFs::new(cx.executor());
2006
2007        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2008        // Using plain text without a configured language, so no outline is available
2009        const LINE: &str = "This is a line of text in the file\n";
2010        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2011        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2012
2013        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2014        let small_content = "fn small_function() { /* small */ }\n";
2015        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2016
2017        fs.insert_tree(
2018            "/project",
2019            json!({
2020                "large_file.txt": large_content.clone(),
2021                "small_file.txt": small_content,
2022            }),
2023        )
2024        .await;
2025
2026        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2027
2028        let (workspace, cx) =
2029            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2030
2031        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2032        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2033
2034        let message_editor = cx.update(|window, cx| {
2035            cx.new(|cx| {
2036                let editor = MessageEditor::new(
2037                    workspace.downgrade(),
2038                    project.downgrade(),
2039                    history_store.clone(),
2040                    None,
2041                    Default::default(),
2042                    Default::default(),
2043                    "Test Agent".into(),
2044                    "Test",
2045                    EditorMode::AutoHeight {
2046                        min_lines: 1,
2047                        max_lines: None,
2048                    },
2049                    window,
2050                    cx,
2051                );
2052                // Enable embedded context so files are actually included
2053                editor
2054                    .prompt_capabilities
2055                    .replace(acp::PromptCapabilities::new().embedded_context(true));
2056                editor
2057            })
2058        });
2059
2060        // Test large file mention
2061        // Get the absolute path using the project's worktree
2062        let large_file_abs_path = project.read_with(cx, |project, cx| {
2063            let worktree = project.worktrees(cx).next().unwrap();
2064            let worktree_root = worktree.read(cx).abs_path();
2065            worktree_root.join("large_file.txt")
2066        });
2067        let large_file_task = message_editor.update(cx, |editor, cx| {
2068            editor.mention_set().update(cx, |set, cx| {
2069                set.confirm_mention_for_file(large_file_abs_path, true, cx)
2070            })
2071        });
2072
2073        let large_file_mention = large_file_task.await.unwrap();
2074        match large_file_mention {
2075            Mention::Text { content, .. } => {
2076                // Should contain some of the content but not all of it
2077                assert!(
2078                    content.contains(LINE),
2079                    "Should contain some of the file content"
2080                );
2081                assert!(
2082                    !content.contains(&LINE.repeat(100)),
2083                    "Should not contain the full file"
2084                );
2085                // Should be much smaller than original
2086                assert!(
2087                    content.len() < large_content.len() / 10,
2088                    "Should be significantly truncated"
2089                );
2090            }
2091            _ => panic!("Expected Text mention for large file"),
2092        }
2093
2094        // Test small file mention
2095        // Get the absolute path using the project's worktree
2096        let small_file_abs_path = project.read_with(cx, |project, cx| {
2097            let worktree = project.worktrees(cx).next().unwrap();
2098            let worktree_root = worktree.read(cx).abs_path();
2099            worktree_root.join("small_file.txt")
2100        });
2101        let small_file_task = message_editor.update(cx, |editor, cx| {
2102            editor.mention_set().update(cx, |set, cx| {
2103                set.confirm_mention_for_file(small_file_abs_path, true, cx)
2104            })
2105        });
2106
2107        let small_file_mention = small_file_task.await.unwrap();
2108        match small_file_mention {
2109            Mention::Text { content, .. } => {
2110                // Should contain the full actual content
2111                assert_eq!(content, small_content);
2112            }
2113            _ => panic!("Expected Text mention for small file"),
2114        }
2115    }
2116
2117    #[gpui::test]
2118    async fn test_insert_thread_summary(cx: &mut TestAppContext) {
2119        init_test(cx);
2120        cx.update(LanguageModelRegistry::test);
2121
2122        let fs = FakeFs::new(cx.executor());
2123        fs.insert_tree("/project", json!({"file": ""})).await;
2124        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2125
2126        let (workspace, cx) =
2127            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2128
2129        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2130        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2131
2132        // Create a thread metadata to insert as summary
2133        let thread_metadata = agent::DbThreadMetadata {
2134            id: acp::SessionId::new("thread-123"),
2135            title: "Previous Conversation".into(),
2136            updated_at: chrono::Utc::now(),
2137        };
2138
2139        let message_editor = cx.update(|window, cx| {
2140            cx.new(|cx| {
2141                let mut editor = MessageEditor::new(
2142                    workspace.downgrade(),
2143                    project.downgrade(),
2144                    history_store.clone(),
2145                    None,
2146                    Default::default(),
2147                    Default::default(),
2148                    "Test Agent".into(),
2149                    "Test",
2150                    EditorMode::AutoHeight {
2151                        min_lines: 1,
2152                        max_lines: None,
2153                    },
2154                    window,
2155                    cx,
2156                );
2157                editor.insert_thread_summary(thread_metadata.clone(), window, cx);
2158                editor
2159            })
2160        });
2161
2162        // Construct expected values for verification
2163        let expected_uri = MentionUri::Thread {
2164            id: thread_metadata.id.clone(),
2165            name: thread_metadata.title.to_string(),
2166        };
2167        let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri());
2168
2169        message_editor.read_with(cx, |editor, cx| {
2170            let text = editor.text(cx);
2171
2172            assert!(
2173                text.contains(&expected_link),
2174                "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}",
2175                expected_link,
2176                text
2177            );
2178
2179            let mentions = editor.mention_set().read(cx).mentions();
2180            assert_eq!(
2181                mentions.len(),
2182                1,
2183                "Expected exactly one mention after inserting thread summary"
2184            );
2185
2186            assert!(
2187                mentions.contains(&expected_uri),
2188                "Expected mentions to contain the thread URI"
2189            );
2190        });
2191    }
2192
2193    #[gpui::test]
2194    async fn test_whitespace_trimming(cx: &mut TestAppContext) {
2195        init_test(cx);
2196
2197        let fs = FakeFs::new(cx.executor());
2198        fs.insert_tree("/project", json!({"file.rs": "fn main() {}"}))
2199            .await;
2200        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2201
2202        let (workspace, cx) =
2203            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2204
2205        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2206        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2207
2208        let message_editor = cx.update(|window, cx| {
2209            cx.new(|cx| {
2210                MessageEditor::new(
2211                    workspace.downgrade(),
2212                    project.downgrade(),
2213                    history_store.clone(),
2214                    None,
2215                    Default::default(),
2216                    Default::default(),
2217                    "Test Agent".into(),
2218                    "Test",
2219                    EditorMode::AutoHeight {
2220                        min_lines: 1,
2221                        max_lines: None,
2222                    },
2223                    window,
2224                    cx,
2225                )
2226            })
2227        });
2228        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
2229
2230        cx.run_until_parked();
2231
2232        editor.update_in(cx, |editor, window, cx| {
2233            editor.set_text("  \u{A0}してhello world  ", window, cx);
2234        });
2235
2236        let (content, _) = message_editor
2237            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
2238            .await
2239            .unwrap();
2240
2241        assert_eq!(content, vec!["してhello world".into()]);
2242    }
2243
2244    #[gpui::test]
2245    async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
2246        init_test(cx);
2247
2248        let fs = FakeFs::new(cx.executor());
2249
2250        let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
2251
2252        fs.insert_tree(
2253            "/project",
2254            json!({
2255                "src": {
2256                    "main.rs": file_content,
2257                }
2258            }),
2259        )
2260        .await;
2261
2262        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2263
2264        let (workspace, cx) =
2265            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2266
2267        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2268        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2269
2270        let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
2271            let workspace_handle = cx.weak_entity();
2272            let message_editor = cx.new(|cx| {
2273                MessageEditor::new(
2274                    workspace_handle,
2275                    project.downgrade(),
2276                    history_store.clone(),
2277                    None,
2278                    Default::default(),
2279                    Default::default(),
2280                    "Test Agent".into(),
2281                    "Test",
2282                    EditorMode::AutoHeight {
2283                        max_lines: None,
2284                        min_lines: 1,
2285                    },
2286                    window,
2287                    cx,
2288                )
2289            });
2290            workspace.active_pane().update(cx, |pane, cx| {
2291                pane.add_item(
2292                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2293                    true,
2294                    true,
2295                    None,
2296                    window,
2297                    cx,
2298                );
2299            });
2300            message_editor.read(cx).focus_handle(cx).focus(window);
2301            let editor = message_editor.read(cx).editor().clone();
2302            (message_editor, editor)
2303        });
2304
2305        cx.simulate_input("What is in @file main");
2306
2307        editor.update_in(cx, |editor, window, cx| {
2308            assert!(editor.has_visible_completions_menu());
2309            assert_eq!(editor.text(cx), "What is in @file main");
2310            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2311        });
2312
2313        let content = message_editor
2314            .update(cx, |editor, cx| editor.contents(false, cx))
2315            .await
2316            .unwrap()
2317            .0;
2318
2319        let main_rs_uri = if cfg!(windows) {
2320            "file:///C:/project/src/main.rs"
2321        } else {
2322            "file:///project/src/main.rs"
2323        };
2324
2325        // When embedded context is `false` we should get a resource link
2326        pretty_assertions::assert_eq!(
2327            content,
2328            vec![
2329                "What is in ".into(),
2330                acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
2331            ]
2332        );
2333
2334        message_editor.update(cx, |editor, _cx| {
2335            editor
2336                .prompt_capabilities
2337                .replace(acp::PromptCapabilities::new().embedded_context(true))
2338        });
2339
2340        let content = message_editor
2341            .update(cx, |editor, cx| editor.contents(false, cx))
2342            .await
2343            .unwrap()
2344            .0;
2345
2346        // When embedded context is `true` we should get a resource
2347        pretty_assertions::assert_eq!(
2348            content,
2349            vec![
2350                "What is in ".into(),
2351                acp::ContentBlock::Resource(acp::EmbeddedResource::new(
2352                    acp::EmbeddedResourceResource::TextResourceContents(
2353                        acp::TextResourceContents::new(file_content, main_rs_uri)
2354                    )
2355                ))
2356            ]
2357        );
2358    }
2359
2360    #[gpui::test]
2361    async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
2362        init_test(cx);
2363
2364        let app_state = cx.update(AppState::test);
2365
2366        cx.update(|cx| {
2367            editor::init(cx);
2368            workspace::init(app_state.clone(), cx);
2369        });
2370
2371        app_state
2372            .fs
2373            .as_fake()
2374            .insert_tree(
2375                path!("/dir"),
2376                json!({
2377                    "test.txt": "line1\nline2\nline3\nline4\nline5\n",
2378                }),
2379            )
2380            .await;
2381
2382        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2383        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2384        let workspace = window.root(cx).unwrap();
2385
2386        let worktree = project.update(cx, |project, cx| {
2387            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2388            assert_eq!(worktrees.len(), 1);
2389            worktrees.pop().unwrap()
2390        });
2391        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2392
2393        let mut cx = VisualTestContext::from_window(*window, cx);
2394
2395        // Open a regular editor with the created file, and select a portion of
2396        // the text that will be used for the selections that are meant to be
2397        // inserted in the agent panel.
2398        let editor = workspace
2399            .update_in(&mut cx, |workspace, window, cx| {
2400                workspace.open_path(
2401                    ProjectPath {
2402                        worktree_id,
2403                        path: rel_path("test.txt").into(),
2404                    },
2405                    None,
2406                    false,
2407                    window,
2408                    cx,
2409                )
2410            })
2411            .await
2412            .unwrap()
2413            .downcast::<Editor>()
2414            .unwrap();
2415
2416        editor.update_in(&mut cx, |editor, window, cx| {
2417            editor.change_selections(Default::default(), window, cx, |selections| {
2418                selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
2419            });
2420        });
2421
2422        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2423        let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2424
2425        // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
2426        // to ensure we have a fixed viewport, so we can eventually actually
2427        // place the cursor outside of the visible area.
2428        let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
2429            let workspace_handle = cx.weak_entity();
2430            let message_editor = cx.new(|cx| {
2431                MessageEditor::new(
2432                    workspace_handle,
2433                    project.downgrade(),
2434                    history_store.clone(),
2435                    None,
2436                    Default::default(),
2437                    Default::default(),
2438                    "Test Agent".into(),
2439                    "Test",
2440                    EditorMode::full(),
2441                    window,
2442                    cx,
2443                )
2444            });
2445            workspace.active_pane().update(cx, |pane, cx| {
2446                pane.add_item(
2447                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2448                    true,
2449                    true,
2450                    None,
2451                    window,
2452                    cx,
2453                );
2454            });
2455
2456            message_editor
2457        });
2458
2459        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2460            message_editor.editor.update(cx, |editor, cx| {
2461                // Update the Agent Panel's Message Editor text to have 100
2462                // lines, ensuring that the cursor is set at line 90 and that we
2463                // then scroll all the way to the top, so the cursor's position
2464                // remains off screen.
2465                let mut lines = String::new();
2466                for _ in 1..=100 {
2467                    lines.push_str(&"Another line in the agent panel's message editor\n");
2468                }
2469                editor.set_text(lines.as_str(), window, cx);
2470                editor.change_selections(Default::default(), window, cx, |selections| {
2471                    selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
2472                });
2473                editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
2474            });
2475        });
2476
2477        cx.run_until_parked();
2478
2479        // Before proceeding, let's assert that the cursor is indeed off screen,
2480        // otherwise the rest of the test doesn't make sense.
2481        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2482            message_editor.editor.update(cx, |editor, cx| {
2483                let snapshot = editor.snapshot(window, cx);
2484                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
2485                let scroll_top = snapshot.scroll_position().y as u32;
2486                let visible_lines = editor.visible_line_count().unwrap() as u32;
2487                let visible_range = scroll_top..(scroll_top + visible_lines);
2488
2489                assert!(!visible_range.contains(&cursor_row));
2490            })
2491        });
2492
2493        // Now let's insert the selection in the Agent Panel's editor and
2494        // confirm that, after the insertion, the cursor is now in the visible
2495        // range.
2496        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2497            message_editor.insert_selections(window, cx);
2498        });
2499
2500        cx.run_until_parked();
2501
2502        message_editor.update_in(&mut cx, |message_editor, window, cx| {
2503            message_editor.editor.update(cx, |editor, cx| {
2504                let snapshot = editor.snapshot(window, cx);
2505                let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
2506                let scroll_top = snapshot.scroll_position().y as u32;
2507                let visible_lines = editor.visible_line_count().unwrap() as u32;
2508                let visible_range = scroll_top..(scroll_top + visible_lines);
2509
2510                assert!(visible_range.contains(&cursor_row));
2511            })
2512        });
2513    }
2514}