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