message_editor.rs

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