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