message_editor.rs

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