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