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