message_editor.rs

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