message_editor.rs

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